diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3b1be540f4..b4abd478dd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `skill-src/` and never commit build outputs): ## Evidence and regression coverage @@ -59,6 +59,10 @@ relevant real-provider or platform workflow, or explain why it was unavailable. limits, security/privacy/containment implications, and meaningful proof gaps. --> - Breaking changes or migration steps: + - Real Hopper/Ghidra, browser, or OS coverage: - Package or release metadata impact: - Security, privacy, process, or containment review: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 740e7f2a5b..b9fe2b375e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: [main, "release/*"] concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} @@ -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() }} @@ -229,7 +231,16 @@ jobs: with: node-version-file: .nvmrc - name: Prepare cross compiler - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64 + run: | + # Slow Azure archive transfers can exhaust the job before compilation. + # Keep the same signed packages and avoid slow HTTP mirror fallback. + for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do + if [ -f "$source" ]; then + sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source" + fi + done + sudo apt-get update + sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64 - run: npm run build:windows-native - run: npm run verify:windows-native:artifact - uses: actions/upload-artifact@v4 @@ -306,14 +317,18 @@ jobs: - run: npm run verify:keyed-archive - run: npm run verify:asset-catalog - run: npm run verify:macos-bundle - - name: Native compiled NIB hierarchy regressions - run: npm run test:focused -- tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts + - name: Native installer and compiled NIB hierarchy regressions + run: npm run test:focused -- tests/boundary/filesystem/installScript.test.ts tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts - name: Real binary metadata and Interface Builder integration run: npm run verify:apple-dispatch && npm run verify:interface-builder - name: Native plist CLI selection regressions 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 + - name: Native Darwin ownership and process cleanup regressions + run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/cli/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts reconstruction-readiness: name: Reconstruction readiness @@ -428,12 +443,16 @@ jobs: tests/boundary/filesystem/safeOutputTree.test.ts tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts tests/boundary/filesystem/setupUnavailableProvider.test.ts + tests/boundary/filesystem/windowsNpxRegistration.test.ts + src/application/ClientRegistrationIdentity.test.ts + tests/boundary/filesystem/webScriptManifestPaths.test.ts + tests/acceptance/webModuleCli.test.ts + tests/acceptance/webSourceLocationCli.test.ts tests/boundary/cli/referenceSourceImportStatus.test.ts src/cliReferenceSourceImportStatus.test.ts tests/boundary/filesystem/asarInventoryFreshness.test.ts tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts tests/conformance/providers/ghidra/windowsCapabilities.test.ts - tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts src/ghidra/GhidraInstallation.test.ts tests/boundary/providers/ghidra/ghidraTransport.test.ts tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts @@ -469,3 +488,13 @@ jobs: cache: npm - run: npm ci - run: npm run docs:check + - name: Generation leaves authored files unchanged + run: git diff --exit-code + - uses: actions/upload-artifact@v4 + with: + name: generated-docs + path: | + docs/public/product-catalog.json + docs/verification/managed-conformance-*.json + skills/reverse-engineer-anything + if-no-files-found: error diff --git a/.github/workflows/real-binary-layout.yml b/.github/workflows/real-binary-layout.yml new file mode 100644 index 0000000000..95a03e165d --- /dev/null +++ b/.github/workflows/real-binary-layout.yml @@ -0,0 +1,78 @@ +name: Real offline binary layout + +on: + workflow_dispatch: + inputs: + large_mcp: + description: "Also verify the complete 65,281-section result over MCP (slow)" + type: boolean + default: false + pull_request: + paths: + - ".github/workflows/real-binary-layout.yml" + - "bridge/pwntools/**" + - "src/native/pwntools/**" + - "src/domain/native/binaryLayout*" + - "src/application/binaryDiagnostics/**" + - "src/contracts/native/binaryDiagnosticsToolContracts.ts" + - "src/composition/binaryDiagnostics.ts" + - "src/server/registerBinaryDiagnosticsTools.ts" + - "src/server/createServer.ts" + - "src/cli/binaryDiagnosticsCommands.ts" + - "src/cli.ts" + - "src/cliCommandNames.ts" + - "src/contracts/toolContracts.ts" + - "src/contracts/toolEffects.ts" + - "src/process/OwnedCommand.ts" + - "src/process/ProviderProcess.ts" + - "src/process/ProviderDeadline.ts" + - "src/process/ProcessOwnership*.ts" + - "src/process/PrivateRuntimeRoot.ts" + - "src/artifacts/readStableArtifact*.ts" + - "src/artifacts/ArtifactReader.ts" + - "src/domain/analysisError*.ts" + - "scripts/verify-binary-layout.mjs" + - "scripts/lib/elf-section-name-fixtures.mjs" + - "scripts/lib/large-result-mcp.mjs" + - "scripts/fixtures/binary-layout*" + - "package.json" + - "package-lock.json" + +permissions: + contents: read + +concurrency: + group: real-binary-layout-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + NODE_OPTIONS: --max-old-space-size=768 + HUSKY: "0" + REA_VERIFY_LARGE_ELF_MCP: ${{ inputs.large_mcp && '1' || '0' }} + +jobs: + real-binary-layout: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - name: Acquire exact upstream libraries in an isolated runner environment + shell: bash + run: | + layout_python_root="${RUNNER_TEMP}/rea-pwntools-layout" + python3 -m venv "${layout_python_root}" + "${layout_python_root}/bin/python" -m pip install --disable-pip-version-check \ + 'pwntools==4.15.0' 'pyelftools==0.33' 'unicorn==2.1.2' + command -v gcc + command -v ld + command -v strip + command -v strace + echo "REA_PWNTOOLS_PYTHON=${layout_python_root}/bin/python" >> "${GITHUB_ENV}" + echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}" + - name: Verify source-owned fixtures through public CLI and MCP + run: npm run verify:binary:layout diff --git a/.github/workflows/real-evm-interface.yml b/.github/workflows/real-evm-interface.yml new file mode 100644 index 0000000000..a493481812 --- /dev/null +++ b/.github/workflows/real-evm-interface.yml @@ -0,0 +1,66 @@ +name: Real offline EVM interface + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/real-evm-interface.yml" + - "src/evm/**" + - "src/domain/evm/**" + - "src/application/evm/**" + - "src/contracts/evm/**" + - "src/composition/evm.ts" + - "src/cli/evmCommands.ts" + - "src/server/registerEvmTools.ts" + - "src/server/createServer.ts" + - "src/cli.ts" + - "src/cliCommandNames.ts" + - "src/contracts/toolContracts.ts" + - "src/contracts/toolEffects.ts" + - "src/process/OwnedCommand.ts" + - "src/process/ProviderProcess.ts" + - "src/process/ProcessOwnership*.ts" + - "src/process/ProviderDeadline.ts" + - "src/process/PrivateRuntimeRoot.ts" + - "src/artifacts/readStableArtifact*.ts" + - "src/artifacts/ArtifactReader.ts" + - "scripts/verify-evm-interface.mjs" + - "scripts/fixtures/evm-interface.sol" + - "third_party/evmole/**" + - "package.json" + - "package-lock.json" + +permissions: + contents: read + +concurrency: + group: real-evm-interface-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + NODE_OPTIONS: --max-old-space-size=768 + HUSKY: "0" + +jobs: + real-evm-interface: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - name: Acquire unchanged fixture compiler in isolated runner storage + shell: bash + run: | + evm_fixture_root="${RUNNER_TEMP}/rea-evm-fixture-compiler" + mkdir -p "${evm_fixture_root}" + npm install --prefix "${evm_fixture_root}" --ignore-scripts --no-audit --no-fund --save-exact solc@0.8.30 + command -v prlimit + command -v strace + echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}" + echo "REA_VERIFY_SOLC_MODULE=${evm_fixture_root}/node_modules/solc/index.js" >> "${GITHUB_ENV}" + - name: Verify source-owned compiled bytecode through public CLI and MCP + run: npm run verify:evm:interface diff --git a/.github/workflows/real-ghidra-windows.yml b/.github/workflows/real-ghidra-windows.yml index 6bebfa6237..42dc38fa46 100644 --- a/.github/workflows/real-ghidra-windows.yml +++ b/.github/workflows/real-ghidra-windows.yml @@ -21,7 +21,14 @@ jobs: with: node-version-file: .nvmrc - name: Prepare cross compiler - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64 + run: | + for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do + if [ -f "$source" ]; then + sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source" + fi + done + sudo apt-get update + sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64 - run: npm run build:windows-native - run: npm run verify:windows-native:artifact - uses: actions/upload-artifact@v4 @@ -53,10 +60,18 @@ jobs: env: REA_ANALYSIS_PROVIDER: ghidra run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log + - name: Verify native x86 Windows Ghidra operations + env: + REA_ANALYSIS_PROVIDER: ghidra + run: npm run verify:ghidra:windows -- --x86 | Tee-Object -FilePath windows-ghidra-x86-proof.log - name: Verify ordinary-user CLI and MCP operations env: REA_ANALYSIS_PROVIDER: ghidra run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log + - name: Verify ordinary-user x86 CLI and MCP operations + env: + REA_ANALYSIS_PROVIDER: ghidra + run: npm run verify:ghidra:windows:package -- --x86 | Tee-Object -FilePath windows-ghidra-x86-package-proof.log - uses: actions/upload-artifact@v4 if: always() with: @@ -64,4 +79,6 @@ jobs: path: | windows-ghidra-proof.log windows-ghidra-package-proof.log + windows-ghidra-x86-proof.log + windows-ghidra-x86-package-proof.log retention-days: 30 diff --git a/.github/workflows/real-recorded-crash.yml b/.github/workflows/real-recorded-crash.yml new file mode 100644 index 0000000000..5138df212f --- /dev/null +++ b/.github/workflows/real-recorded-crash.yml @@ -0,0 +1,99 @@ +name: Real recorded crash evidence + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/real-recorded-crash.yml" + - "bridge/pwntools/**" + - "bridge/pwndbg/**" + - "src/native/pwntools/**" + - "src/native/pwndbg/**" + - "src/domain/native/recordedCrash*" + - "src/application/binaryDiagnostics/**" + - "src/contracts/native/binaryDiagnosticsToolContracts.ts" + - "src/contracts/toolEffects.ts" + - "src/contracts/toolOutputSchemaGroups.ts" + - "src/composition/binaryDiagnostics.ts" + - "src/server/registerRecordedCrashTools.ts" + - "src/server/createServer.ts" + - "src/server/sessionAvailabilityPolicy.ts" + - "src/cli/binaryDiagnosticsCommands.ts" + - "src/cliCommandNames.ts" + - "src/process/**" + - "src/artifacts/readStableArtifact*.ts" + - "src/domain/analysisError*.ts" + - "scripts/verify-recorded-crash.mjs" + - "scripts/verify/recorded-crash/**" + - "scripts/lib/recorded-core-fixtures.py" + - "scripts/fixtures/recorded-crash.c" + - "third_party/pwndbg/**" + - "package.json" + - "package-lock.json" + +permissions: + contents: read + +concurrency: + group: real-recorded-crash-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + NODE_OPTIONS: --max-old-space-size=768 + HUSKY: "0" + UV_CONCURRENT_BUILDS: "1" + UV_CONCURRENT_INSTALLS: "1" + UV_PYTHON_DOWNLOADS: never + +jobs: + real-recorded-crash: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - name: Install GDB on the disposable CI runner + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install --yes --no-install-recommends gdb + - name: Acquire exact unchanged upstreams in isolated runner storage + shell: bash + run: | + command -v gcc + command -v gdb + command -v strace + core_decoder_root="${RUNNER_TEMP}/rea-core-decoder" + core_pwndbg_root="${RUNNER_TEMP}/rea-core-pwndbg" + core_pwndbg_venv="${RUNNER_TEMP}/rea-core-pwndbg-venv" + python3 -m venv "${core_decoder_root}" + "${core_decoder_root}/bin/python" -m pip install --disable-pip-version-check \ + 'pwntools==4.15.0' 'pyelftools==0.33' 'unicorn==2.1.2' 'uv==0.12.0' + git init "${core_pwndbg_root}" + git -C "${core_pwndbg_root}" remote add origin https://github.com/pwndbg/pwndbg.git + git -C "${core_pwndbg_root}" fetch --depth=1 origin 55596aa676a2756efb8d67034f31675170eabe48 + git -C "${core_pwndbg_root}" checkout --detach FETCH_HEAD + test "$(git -C "${core_pwndbg_root}" rev-parse HEAD)" = 55596aa676a2756efb8d67034f31675170eabe48 + UV_PROJECT_ENVIRONMENT="${core_pwndbg_venv}" \ + "${core_decoder_root}/bin/uv" sync --project "${core_pwndbg_root}" \ + --frozen --no-editable --no-default-groups --python /usr/bin/python3 --no-managed-python + test -z "$(git -C "${core_pwndbg_root}" status --porcelain --untracked-files=no)" + echo "REA_PWNTOOLS_PYTHON=${core_decoder_root}/bin/python" >> "${GITHUB_ENV}" + echo "REA_PWNDBG_GDB=$(command -v gdb)" >> "${GITHUB_ENV}" + echo "REA_PWNDBG_GDBINIT=${core_pwndbg_root}/gdbinit.py" >> "${GITHUB_ENV}" + echo "REA_PWNDBG_VENV_PATH=${core_pwndbg_venv}" >> "${GITHUB_ENV}" + echo "REA_VERIFY_STRACE_COMMAND=$(command -v strace)" >> "${GITHUB_ENV}" + - name: Build and install the exact package in isolated runner storage + shell: bash + run: | + npm run build:cached + npm pack --ignore-scripts --json --pack-destination "${RUNNER_TEMP}" > "${RUNNER_TEMP}/rea-core-pack.json" + core_package_name=$(node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"))[0].filename' "${RUNNER_TEMP}/rea-core-pack.json") + npm install --prefix "${RUNNER_TEMP}/rea-core-installed" --ignore-scripts --no-fund --no-audit "${RUNNER_TEMP}/${core_package_name}" + - name: Verify installed CLI and MCP against the source-owned recording + shell: bash + run: node scripts/verify-recorded-crash.mjs "${RUNNER_TEMP}/rea-core-installed/node_modules/rea-agents/scripts/rea.mjs" diff --git a/.github/workflows/real-web-network-captures.yml b/.github/workflows/real-web-network-captures.yml new file mode 100644 index 0000000000..91681043ac --- /dev/null +++ b/.github/workflows/real-web-network-captures.yml @@ -0,0 +1,75 @@ +name: Real historical web network captures + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/real-web-network-captures.yml" + - "bridge/mitmproxy/**" + - "src/browser/history/**" + - "src/domain/webNetworkCapture*.ts" + - "src/domain/analysisError*.ts" + - "src/contracts/errorSchemas.ts" + - "src/domain/explicitSensitive*.ts" + - "src/application/WebNetworkCapture*" + - "src/composition/webNetworkCaptures.ts" + - "src/cli/webNetworkCaptureCommands.ts" + - "src/server/registerWebNetworkCaptureTool.ts" + - "src/server/createServer.ts" + - "src/server/toolRegistrationOptions.ts" + - "src/cli.ts" + - "src/cliCommandNames.ts" + - "src/contracts/toolContracts.ts" + - "src/contracts/toolEffects.ts" + - "src/process/OwnedCommand.ts" + - "src/process/ProviderProcess.ts" + - "src/process/ProviderDeadline.ts" + - "src/process/ProcessOwnership*.ts" + - "src/process/PrivateRuntimeRoot.ts" + - "src/process/capture/ProcessCaptureLifecycle.ts" + - "src/artifacts/readStableArtifact*.ts" + - "src/artifacts/ArtifactReader.ts" + - "src/contracts/webNetworkCaptureToolContracts.ts" + - "scripts/verify-web-network-captures.mjs" + - "scripts/fixtures/generate-mitmproxy-capture.py" + - "package.json" + - "package-lock.json" + +permissions: + contents: read + +concurrency: + group: real-web-network-captures-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + HUSKY: "0" + NODE_OPTIONS: --max-old-space-size=768 + +jobs: + real-web-network-captures: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + - name: Use pinned package manager + run: npm install --global npm@11.16.0 + - run: npm ci + - name: Acquire fixed upstream native profile in runner temporary storage + shell: bash + run: | + capture_tool_root="${RUNNER_TEMP}/rea-mitmproxy-12.2.3" + mkdir -p "${capture_tool_root}" + curl --fail --location --retry 3 --max-time 180 \ + https://downloads.mitmproxy.org/12.2.3/mitmproxy-12.2.3-linux-x86_64.tar.gz \ + --output "${capture_tool_root}/mitmproxy.tar.gz" + echo '2e95286b618fa6fd33e5e62a78c2e5112571d85f42ec2bac29b97ee242bdb5c5 mitmproxy.tar.gz' \ + | (cd "${capture_tool_root}" && sha256sum --check --strict) + tar -xzf "${capture_tool_root}/mitmproxy.tar.gz" -C "${capture_tool_root}" + echo "REA_MITMDUMP_COMMAND=${capture_tool_root}/mitmdump" >> "${GITHUB_ENV}" + - name: Verify upstream producer fixtures through public CLI and MCP + run: npm run verify:web:network-captures diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5995f4c7e8..e10fc62061 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,14 +1,24 @@ name: Release on: - push: - branches: [main] + 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: @@ -20,85 +30,163 @@ 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: + RELEASE_BRANCH: ${{ inputs.release_branch }} + RELEASE_PHASE: ${{ inputs.phase }} + run: | + 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 + exit 1 + fi + + - 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 + + - name: Check out release controller and Git history + uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Set up Node.js for checkpoint validation + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + package-manager-cache: false + + - name: Install checkpoint validation dependencies + run: npm ci + + - name: Validate checkpoint version and ancestry before release creation + env: + RELEASE_PHASE: ${{ inputs.phase }} + RELEASE_BRANCH: ${{ inputs.release_branch }} + SOURCE_SHA: ${{ steps.branch.outputs.sha }} + run: >- + node scripts/verify-release-checkpoint.mjs + --phase "$RELEASE_PHASE" --release-branch "$RELEASE_BRANCH" + --source-sha "$SOURCE_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: ${{ inputs.release_branch }} + skip-github-release: ${{ inputs.phase == 'prepare' }} + skip-github-pull-request: ${{ inputs.phase == 'publish' }} + + - 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 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 }} + fetch-depth: 0 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: Validate prepared release version and migration notes + if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + env: + RELEASE_BRANCH: ${{ inputs.release_branch }} + SOURCE_SHA: ${{ steps.branch.outputs.sha }} + run: >- + node scripts/verify-release-checkpoint.mjs + --phase prepare --stage candidate --release-branch "$RELEASE_BRANCH" + --source-sha "$SOURCE_SHA" + - 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' - shell: bash - env: - # Use the same identity for the follow-up commit so it also triggers CI. - GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} - RELEASE_PR_JSON: ${{ steps.release.outputs.pr }} - run: | - release_pr_branch="$(node --input-type=module -e ' - const value = JSON.parse(process.env.RELEASE_PR_JSON ?? ""); - if ( - typeof value !== "object" || - value === null || - typeof value.headBranchName !== "string" || - value.headBranchName.length === 0 - ) { - process.exit(1); - } - process.stdout.write(value.headBranchName); - ')" - changed_paths="$(git diff --name-only)" - if [[ -z "${changed_paths}" ]]; then - echo "Release documentation is already canonical." - exit 0 - fi - if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then - echo "docs:generate changed unexpected tracked paths:" >&2 - printf '%s\n' "${changed_paths}" >&2 - exit 1 - fi - git diff --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -- docs/product-catalog.json - git commit -m "chore: normalize release product catalog" - gh auth setup-git - git push origin "HEAD:${release_pr_branch}" + - name: Validate generated release documentation + if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + run: npm run docs:check && git diff --exit-code 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 id-token: write steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.release-please.outputs.sha }} + persist-credentials: false - uses: actions/setup-node@v6 with: @@ -135,28 +223,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 @@ -164,7 +256,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{ needs.release-please.outputs.tag_name }} + ref: ${{ needs.release-please.outputs.sha }} persist-credentials: false - name: Verify package and registry metadata versions diff --git a/.github/workflows/website-check.yml b/.github/workflows/website-check.yml index 4f410ef62c..2fc7a6874d 100644 --- a/.github/workflows/website-check.yml +++ b/.github/workflows/website-check.yml @@ -2,7 +2,7 @@ name: Website checks on: pull_request: - branches: [main] + branches: [main, "release/*"] paths: - "website/**" - "scripts/verify-website.py" diff --git a/.gitignore b/.gitignore index 330e03a16d..a362e63e81 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ test-results.xml .turbo/ .cache/ docs/.vitepress/cache/ +docs/.vitepress/dist/ # OS .DS_Store @@ -48,3 +49,13 @@ 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 + +# Derived catalogs, portable conformance projections, and packaged skill bundles +# (regenerated from this checkout by `npm run build:cached`; never commit) +/skills/ +/docs/public/product-catalog.json +/docs/verification/managed-conformance-manifest.json +/docs/verification/managed-conformance-ledger.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 411256bcab..a3a12f4948 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.1.0" + ".": "6.0.0" } diff --git a/AGENTS.md b/AGENTS.md index a3f3e35e4a..f7f8d9ea16 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Product Direction -REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers. +REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers. Prioritize: @@ -24,7 +24,8 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure - `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points. - `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. +- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections. +- `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 @@ -35,7 +36,7 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure - `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`). - `npm run check:fast`: run cached typecheck and lint checks. - `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time. -- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them. +- `npm run docs:check`: build and validate generated documents for the current checkout; `npm run docs:generate` regenerates them. CI retains ignored outputs as artifacts and does not push snapshot commits to feature branches. - For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification. - Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dc158cb71..28266af873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,363 @@ # Changelog +## [6.0.0](https://github.com/morluto/rea/compare/rea-agents-5.0.0...rea-agents-6.0.0) (2026-10-08) + + +### ⚠ BREAKING CHANGES + +* **contracts:** MCP filesystem inputs now require absolute host paths, including target/snapshot paths, evidence bundle paths, managed and firmware inputs, browser/Electron launch paths and expected server paths. Calls such as `open_binary({"path":"./app"})` must send an absolute path such as `/home/analyst/app` or `C:\analysis\app`. Optional paths remain optional. CLI operator-relative paths continue to work. See [#948](https://github.com/morluto/rea/pull/948) and [#1004](https://github.com/morluto/rea/pull/1004). + +### Features + +* **artifacts:** trace Mach-O dylib resolution in bundles ([78d594c](https://github.com/morluto/rea/commit/78d594cacd376725f833a99402069c103aae9aac)) +* **browser:** inspect historical web network captures ([#992](https://github.com/morluto/rea/issues/992)) ([2bf3663](https://github.com/morluto/rea/commit/2bf3663f654386c9fd8c1437c49659df97bd4697)) +* **evm:** inspect offline bytecode interfaces through EVMole ([#1051](https://github.com/morluto/rea/issues/1051)) ([7af7bc8](https://github.com/morluto/rea/commit/7af7bc88ce0e8f084eaa0db1e8f834d495440fd9)) +* **ghidra:** support native x86 PE applications on Windows ([#1030](https://github.com/morluto/rea/issues/1030)) ([c6dad93](https://github.com/morluto/rea/commit/c6dad932084032bcb32acc61e8066a7784497710)) +* **native:** inspect offline ELF layout through pwntools ([#1037](https://github.com/morluto/rea/issues/1037)) ([4fc6565](https://github.com/morluto/rea/commit/4fc6565b0ffb36d7105b3524e08d0c5d603ae07d)) +* **native:** inspect recorded crashes through pwntools and pwndbg ([#1054](https://github.com/morluto/rea/issues/1054)) ([163ee09](https://github.com/morluto/rea/commit/163ee09a7425479385e0ed0b5d74043e2587eab2)) +* **native:** observe function and Objective-C method calls under LLDB ([1d13cef](https://github.com/morluto/rea/commit/1d13cef5f46f5e07f594bde7b1a4725f189b9a5e)) + + +### Bug Fixes + +* **dotnet:** validate managed metadata and CIL boundaries ([#938](https://github.com/morluto/rea/pull/938)) ([bda4ada](https://github.com/morluto/rea/commit/bda4ada7fb6bd7dd70a444bb3c57e34f7f5cc00b)) + +* **build:** clarify generated-file check hint and drop redundant catalog step ([#998](https://github.com/morluto/rea/pull/998)) ([6f0ef16](https://github.com/morluto/rea/commit/6f0ef160501ea3b24dfa2939dfcd3eea0dc13ee7)) + +* **release:** extend npm propagation window and improve diagnostics ([#943](https://github.com/morluto/rea/pull/943)) ([06b047a](https://github.com/morluto/rea/commit/06b047a0929369d5b8635b3651da115c8fcd53a7)) + +* **contracts:** require absolute local paths for session filesystem inputs ([#948](https://github.com/morluto/rea/pull/948)) ([5b172b1](https://github.com/morluto/rea/commit/5b172b1042fc09a4e12d956018f15d86cf397ffe)) + +* **ci:** generate source catalog before shard tests ([#1002](https://github.com/morluto/rea/pull/1002)) ([eb8f649](https://github.com/morluto/rea/commit/eb8f6491a88bfa79d13f7923e4ef94d791f6ba4d)) + +* **contracts:** require absolute local paths for remaining caller-supplied file inputs ([#1004](https://github.com/morluto/rea/pull/1004)) ([cbc0dd1](https://github.com/morluto/rea/commit/cbc0dd1e7ca36fa3504a9ea8eae303a0876b176b)) + +* **cli:** classify ENOTDIR JSON input failures ([#1000](https://github.com/morluto/rea/pull/1000)) ([a6132be](https://github.com/morluto/rea/commit/a6132be0546d6a9e3fa83426c8b59b1280b3e5bd)) + +* **browser:** preserve authorized redirect hops ([#986](https://github.com/morluto/rea/pull/986)) ([ae0f725](https://github.com/morluto/rea/commit/ae0f725fad368730c6198d49716ccc410bca0536)) + +* **mcp:** reject unknown MCP tool inputs ([#975](https://github.com/morluto/rea/pull/975)) ([fe64fd6](https://github.com/morluto/rea/commit/fe64fd65d93eff202ad90145544dc9ff84651974)) + +* **release:** publish from explicit source checkpoints ([#1003](https://github.com/morluto/rea/pull/1003)) ([64c7188](https://github.com/morluto/rea/commit/64c71880ae4f7bc658ffce46eb729c4141dbdf19)) + +* **release:** publish from explicit source checkpoints ([#1006](https://github.com/morluto/rea/pull/1006)) ([b4fc0ec](https://github.com/morluto/rea/commit/b4fc0ecaa1f6b912dc27880e6b6127ace47abc2e)) + +* **javascript:** bound semantic node projection for resource safety ([#966](https://github.com/morluto/rea/pull/966)) ([96a46ad](https://github.com/morluto/rea/commit/96a46ada0a36f0349f2b0fc20b4c932401b4ad84)) + +* **ci:** avoid slow Azure archive for the native cross compiler ([#1016](https://github.com/morluto/rea/issues/1016)) ([51956c1](https://github.com/morluto/rea/commit/51956c1a49a7dc9ed70a6fba480a670a7e7b2b9b)) +* **ci:** decouple stdio smoke tests from Linux display probes ([#1014](https://github.com/morluto/rea/issues/1014)) ([93731e1](https://github.com/morluto/rea/commit/93731e17ddea76db86dfa84bb54536dcf8ebfa0b)) +* **cli:** stream large JavaScript JSON results ([#1024](https://github.com/morluto/rea/issues/1024)) ([0e25bef](https://github.com/morluto/rea/commit/0e25befe78f220691abe5d0291cc04a75c9cc877)) +* **javascript:** complete large-ASAR analysis with file-local projection ([#1055](https://github.com/morluto/rea/issues/1055)) ([b9fb438](https://github.com/morluto/rea/commit/b9fb43883570bef7cb29fd16f27c25d5a7e5bb54)) +* **javascript:** recognize digits in artifact URI schemes ([#1045](https://github.com/morluto/rea/issues/1045)) ([03424ae](https://github.com/morluto/rea/commit/03424ae91ef22efd57db1bdde9379a3ac450b8f7)) +* **javascript:** reconstruct HTML script loads accurately ([#1042](https://github.com/morluto/rea/issues/1042)) ([1154f2f](https://github.com/morluto/rea/commit/1154f2f03c0b0bb6cb6c5da09b8d8e1c837febc4)) +* **javascript:** report application analysis progress on the CLI ([#1008](https://github.com/morluto/rea/issues/1008)) ([7b18c58](https://github.com/morluto/rea/commit/7b18c581409848077de7a9cb6d7d1a0b91a02e12)) +* **javascript:** resolve module URLs and reject invalid exports targets ([#1031](https://github.com/morluto/rea/issues/1031)) ([8d5c88f](https://github.com/morluto/rea/commit/8d5c88f17cde4ab4fa54c5142f0a45e612619a53)) +* **javascript:** resolve root package exports arrays ([#1046](https://github.com/morluto/rea/issues/1046)) ([53409ab](https://github.com/morluto/rea/commit/53409ab1968d60c77f98feedefe237a08859a95c)) +* **mcp:** advertise JSON values without recursive schemas ([#995](https://github.com/morluto/rea/issues/995)) ([7425f70](https://github.com/morluto/rea/commit/7425f70fd0247a7ea7192deb959fd6f48ba83a21)), closes [#919](https://github.com/morluto/rea/issues/919) +* **mcp:** bound capture comparison input schema depth ([#1005](https://github.com/morluto/rea/issues/1005)) ([6b55de0](https://github.com/morluto/rea/commit/6b55de017badc9066642e1ff2561eb1da0ff93f8)) +* **process:** preserve partial evidence and verify detached cleanup ([#1011](https://github.com/morluto/rea/issues/1011)) ([63cf28f](https://github.com/morluto/rea/commit/63cf28fd8f8acaf656e993300a2a24280b2fd692)) +* **release:** support built-in credentials and refresh release boundary ([6a278a6](https://github.com/morluto/rea/commit/6a278a68dd4fa69b9153697ecb7bba7de4c55c32)) +* **release:** support default credentials and record 5.0.0 boundary ([#1012](https://github.com/morluto/rea/issues/1012)) ([6a278a6](https://github.com/morluto/rea/commit/6a278a68dd4fa69b9153697ecb7bba7de4c55c32)) +* **release:** sync 5.0.0 checkpoint back to main ([966cca8](https://github.com/morluto/rea/commit/966cca8efe5ed08b3285cd06409688d871eb5e6b)) +* **snapshot:** replay validated workflow evidence ([#984](https://github.com/morluto/rea/issues/984)) ([1d62e54](https://github.com/morluto/rea/commit/1d62e54e1f976e64130e4e9b651c3180aa4fa727)) +* **windows:** allow Java to read retained target snapshots ([#1027](https://github.com/morluto/rea/issues/1027)) ([28d5c39](https://github.com/morluto/rea/commit/28d5c397c83f901e4086083868041711371ad32a)) + + +### Performance Improvements + +* **artifacts:** replace quadratic ZIP overlap checks ([#1013](https://github.com/morluto/rea/issues/1013)) ([76e7e51](https://github.com/morluto/rea/commit/76e7e510bb2d7d37728ca50749d524beb5890cf8)) +* **javascript:** cache canonical export comparison sort keys ([#1028](https://github.com/morluto/rea/issues/1028)) ([63ba742](https://github.com/morluto/rea/commit/63ba7424c0ab69b94397e8913f8e231de111583f)) + + +### Code Refactoring + +* **build:** generate MCP tool catalog at build time instead of committing it ([#997](https://github.com/morluto/rea/pull/997)) ([35a4e18](https://github.com/morluto/rea/commit/35a4e18d3028014e24d2e209ec99a590fde63152)) + +* **process:** separate capture implementation from analyst workflows ([#988](https://github.com/morluto/rea/pull/988)) ([6aa9afb](https://github.com/morluto/rea/commit/6aa9afb5493a97801eb1126a0dcf11f9705165db)) + +* **artifacts:** move inventory and extraction into provider owners ([#1032](https://github.com/morluto/rea/issues/1032)) ([6e4535c](https://github.com/morluto/rea/commit/6e4535c1011f1982e9ab07b7ebcd600c19cd494d)) +* remove abandoned test-only analysis scaffolding ([c93422a](https://github.com/morluto/rea/commit/c93422a898fdbedb7d28709144b4337a2669440b)) + + +### Documentation + +* improve Korean wording and particles ([#1047](https://github.com/morluto/rea/issues/1047)) ([c5d165b](https://github.com/morluto/rea/commit/c5d165bd03fed95a0b0105661d3ba7e0efbfa449)) +* simplify the English README and organize guides ([#1043](https://github.com/morluto/rea/issues/1043)) ([28f10cf](https://github.com/morluto/rea/commit/28f10cf2bc7b7c590700b50d47072015f3e3c66e)) + + +### Tests + +* **process:** wait for observable snapshot ctime changes ([#999](https://github.com/morluto/rea/pull/999)) ([161ed91](https://github.com/morluto/rea/commit/161ed9157134d5c3b0fbae19ceb756ac58a26c04)) + +* **browser:** consolidate CDP workflows and colocate parser coverage ([2682904](https://github.com/morluto/rea/commit/26829045d0f0e744bb60d6babc2dc3312f4bfa53)) +* **ci:** stabilize browser source identity fixture delivery ([#1023](https://github.com/morluto/rea/issues/1023)) ([ed725da](https://github.com/morluto/rea/commit/ed725dace4a689e4e5c804dc6bf06c7fc663c7c9)) +* **cli:** reduce repeated startup and filesystem setup ([d88d6c1](https://github.com/morluto/rea/commit/d88d6c1897740ccbd6c57f4ba01861fc751fb3a8)) +* consolidate service and process fixtures ([f8a53a8](https://github.com/morluto/rea/commit/f8a53a8c065f1ccbdaa051a4e1ef267a2047d759)) +* **javascript:** consolidate source analysis and export regressions ([f517a5a](https://github.com/morluto/rea/commit/f517a5a1d7c0766ed50f8dfd5ee69e65525ac744)) +* **javascript:** cover plain long-line loader identities ([#1025](https://github.com/morluto/rea/issues/1025)) ([984fc27](https://github.com/morluto/rea/commit/984fc27ce19d57f708bdae688a258cf8663cdea6)) +* **mcp:** consolidate SDK schema checks and scenario ports ([9c3479e](https://github.com/morluto/rea/commit/9c3479ed95100d8ee5ed4d9158a4550e4ac2a752)) +* **providers:** verify framing through client socket boundaries ([1fd1989](https://github.com/morluto/rea/commit/1fd1989d342375a01b6d14b282ad58001b7d1800)) +* prune redundant conformance checks and document test priorities ([aafdf8a](https://github.com/morluto/rea/commit/aafdf8a01b42fb6fd927c0f2a66e758e1a510abc)) + +## [5.0.0](https://github.com/morluto/rea/compare/rea-agents-4.1.0...rea-agents-5.0.0) (2026-10-07) + + +### ⚠ BREAKING CHANGES + +* **native:** preserve incomplete native UI captures ([#991](https://github.com/morluto/rea/issues/991)) +* **android:** Android analysis now requires a full JDK 17 or newer with its compiler module for the packaged Java source bridge. + +### Features + +* **apple:** project macOS bundle anatomy in application graphs ([88c940b](https://github.com/morluto/rea/commit/88c940b165d788a491e212ea93e00462c4dde6a2)) +* **apple:** project macOS bundle anatomy in application graphs ([dd3e7ae](https://github.com/morluto/rea/commit/dd3e7aef0d52c478f6cdf475501c42ebe7f5c97e)) +* **application:** wire Android and Apple inventory graph tools ([#852](https://github.com/morluto/rea/issues/852)) ([e0fd820](https://github.com/morluto/rea/commit/e0fd8207a1fb738bcc34f204b55c4fa154f5f02d)) +* **browser:** attribute native execution and listener sources ([#917](https://github.com/morluto/rea/issues/917)) ([b5afa5f](https://github.com/morluto/rea/commit/b5afa5f91eed9ae31d670aeab9bbf5112d790ec2)) +* **browser:** capture correlated network content and lifecycle evidence ([#797](https://github.com/morluto/rea/issues/797)) ([f9eaf05](https://github.com/morluto/rea/commit/f9eaf056ef58215094513176a70fabae0740081f)) +* **browser:** trace captured website module imports ([#879](https://github.com/morluto/rea/issues/879)) ([01be082](https://github.com/morluto/rea/commit/01be082e9e11c78b91828c06a9c543c754783c07)) +* **browser:** trace captured website source-map locations ([#897](https://github.com/morluto/rea/issues/897)) ([f430c41](https://github.com/morluto/rea/commit/f430c416c72d444fbe521e0263f8935da4cdb040)) +* **javascript:** recover readable modules with Wakaru ([#848](https://github.com/morluto/rea/issues/848)) ([166dcd2](https://github.com/morluto/rea/commit/166dcd21d06f687a9c480a6a6d36beb3b4954dd0)) +* **native:** decode chained fixups, binds, ObjC categories and properties ([a657557](https://github.com/morluto/rea/commit/a65755728f7a10d984514c75fa863b84ddcdaee9)) +* **native:** decode chained fixups, binds, ObjC categories and properties ([3e24a72](https://github.com/morluto/rea/commit/3e24a72465fe6c628db1f836e93d74d7f4a92b80)) +* **native:** open iOS-style and iOS-on-Mac wrapper app bundles ([2ed89e6](https://github.com/morluto/rea/commit/2ed89e6529924248d2e21084f8730bedbb16bc3f)) +* **native:** open iOS-style and iOS-on-Mac wrapper app bundles ([aeebba3](https://github.com/morluto/rea/commit/aeebba391ee5c288b75cd828e7d18d32b40aa698)) +* **web:** export captured scripts for JavaScript application analysis ([#801](https://github.com/morluto/rea/issues/801)) ([472b72a](https://github.com/morluto/rea/commit/472b72a53068a8e2ec78fe239d687023482daaa7)) +* **website:** add a shared back-to-top link ([5047373](https://github.com/morluto/rea/commit/504737300632d8644fbd6c658d2c97e38bb54c3b)) +* **website:** introduce REA and the DX-Ball reconstruction ([#906](https://github.com/morluto/rea/issues/906)) ([4e3953c](https://github.com/morluto/rea/commit/4e3953cc45ce9c21786a7a6fc495d2350aa85d24)) + + +### Bug Fixes + +* address review nits from batch integration follow-ups ([#818](https://github.com/morluto/rea/issues/818)) ([5a0e0ba](https://github.com/morluto/rea/commit/5a0e0ba3b81ea0ec336addb07da98ec7bc0a35d3)) +* **android:** align runtime suffix inference ([#977](https://github.com/morluto/rea/issues/977)) ([fb3749a](https://github.com/morluto/rea/commit/fb3749aff692a7a8225dff00a4516b48a76866b6)) +* **android:** bound fragmented JADX MCP frame memory ([#937](https://github.com/morluto/rea/issues/937)) ([9e39795](https://github.com/morluto/rea/commit/9e397958951edb543134e3ad57e7ab253d6df21a)) +* **android:** preserve JVM settings and reuse metadata sessions ([#908](https://github.com/morluto/rea/issues/908)) ([eab34a9](https://github.com/morluto/rea/commit/eab34a9d8c5eebad6b04b85750be0276d679c3f9)) +* **apple:** attribute only application-root components in macOS inventories ([5524970](https://github.com/morluto/rea/commit/55249704c5c83cbf8d20ac7869863cc196da00a6)) +* **apple:** count only real framework version directories ([2e81e86](https://github.com/morluto/rea/commit/2e81e867b24da8314f666f62d154de5803040cdf)) +* **apple:** pair bridge hypotheses within one application root ([7acf4e7](https://github.com/morluto/rea/commit/7acf4e741a6a314ea7aabb18fc757e196158ff76)) +* **apple:** pair bridge hypotheses within one application root ([9226ade](https://github.com/morluto/rea/commit/9226ade5114f287eb8e1b00b08dd91da1cc66003)) +* **apple:** traverse keyed archive hierarchy collections ([#976](https://github.com/morluto/rea/issues/976)) ([71ac83f](https://github.com/morluto/rea/commit/71ac83f3dd376135098f460c86e7897183456ec5)) +* **artifacts:** accept APFS image disks ejected with their container ([d93ebec](https://github.com/morluto/rea/commit/d93ebec9df951d40e66ced667f07fadd2dd5d25a)) +* **artifacts:** accept APFS image disks ejected with their container ([b72c079](https://github.com/morluto/rea/commit/b72c079a9a90c374bd6dd30ff8c4387306b8f59b)) +* **artifacts:** accept the bundle subject when extracting an app bundle ([#891](https://github.com/morluto/rea/issues/891)) ([37d2eba](https://github.com/morluto/rea/commit/37d2ebac4c2a524eec6e32c4c57cbbb632491ad8)) +* **artifacts:** decode flat keyed-archive NIBs with data and shared references ([b3f013f](https://github.com/morluto/rea/commit/b3f013fa183e8351319a053575aeee5df337c276)) +* **artifacts:** decode flat keyed-archive NIBs with data and shared references ([2a4667b](https://github.com/morluto/rea/commit/2a4667bb79117b202abb009c0bb9ebfdfde57158)) +* **artifacts:** harden reader lifecycle boundaries ([#951](https://github.com/morluto/rea/issues/951)) ([2366bda](https://github.com/morluto/rea/commit/2366bda37b1084016d42a0d06920dc57e7c7723f)) +* **artifacts:** keep the post-snapshot detach check out of provenance ([62634e4](https://github.com/morluto/rea/commit/62634e4c0177bde8a08decc356da98fbe25d2000)) +* **artifacts:** preserve reader failure diagnostics ([#987](https://github.com/morluto/rea/issues/987)) ([fc4a9a0](https://github.com/morluto/rea/commit/fc4a9a05922ce8474fd0717bc800343b0a95bd41)) +* **artifacts:** project compiled AppKit actions from the control to its target ([b08228d](https://github.com/morluto/rea/commit/b08228d19f8a0f6dbc940e8d322ef2bb42d2ce1a)) +* **artifacts:** project compiled AppKit actions from the control to its target ([#905](https://github.com/morluto/rea/issues/905)) ([900d7c8](https://github.com/morluto/rea/commit/900d7c8606cbc1ccbca818827a55bf2eab28bca4)) +* **artifacts:** project UIKit runtime outlet and event connections ([aa8082e](https://github.com/morluto/rea/commit/aa8082ea6c0898feb4d842135aea6b9539f769f8)) +* **artifacts:** project UIKit runtime outlet and event connections ([8b24387](https://github.com/morluto/rea/commit/8b243877c22a5550d005e7b8abf93791ce8566b9)) +* **artifacts:** read AppKit connectors from flat keyed-archive NIBs ([#900](https://github.com/morluto/rea/issues/900)) ([27c272b](https://github.com/morluto/rea/commit/27c272bd9a42e7235c7991881196c60dbc126714)) +* **artifacts:** read keyed archive dictionaries keyed UID as dictionaries ([cfd2b0c](https://github.com/morluto/rea/commit/cfd2b0c75dfb9d51b936788a46d0338145803ece)) +* **artifacts:** read keyed archive dictionaries keyed UID as dictionaries ([35c82b9](https://github.com/morluto/rea/commit/35c82b93eb2579687bb446c3a8a2a9f1a7563d28)) +* **artifacts:** report which path constraint failed ([#847](https://github.com/morluto/rea/issues/847)) ([3f1a77f](https://github.com/morluto/rea/commit/3f1a77f68ec4355d8b792ccb2970bc03485889c9)) +* **browser:** distinguish computed call keys from identifiers ([#808](https://github.com/morluto/rea/issues/808)) ([02beb74](https://github.com/morluto/rea/commit/02beb745358f02bdaabed4bb523c5e3527a9841e)) +* **browser:** distinguish WebMCP registration frame owners ([#813](https://github.com/morluto/rea/issues/813)) ([b3b44d8](https://github.com/morluto/rea/commit/b3b44d8a2d8b2aa70a8ed8b9cddf365865fcc53f)) +* **browser:** fingerprint cookies at the selected page URL ([#763](https://github.com/morluto/rea/issues/763)) ([78014e3](https://github.com/morluto/rea/commit/78014e3d149f762c848581a0aac7e44f75d51225)) +* **browser:** identify NodeNext TypeScript original source artifacts ([#811](https://github.com/morluto/rea/issues/811)) ([e84ad86](https://github.com/morluto/rea/commit/e84ad86a7578fb46568663aca6fe6a6f3986cc8d)) +* **browser:** preserve evidence through deep AST analysis ([#924](https://github.com/morluto/rea/issues/924)) ([f826aa8](https://github.com/morluto/rea/commit/f826aa893fa7d55e87d907f0c94c39421a166dd8)) +* **browser:** preserve form destinations and satisfies mutation uncertainty ([#825](https://github.com/morluto/rea/issues/825)) ([6dd719e](https://github.com/morluto/rea/commit/6dd719eaf605b37cd5dad6d01d33527ca9d59f06)), closes [#820](https://github.com/morluto/rea/issues/820) +* **browser:** preserve Link header value delimiters ([#805](https://github.com/morluto/rea/issues/805)) ([dd36405](https://github.com/morluto/rea/commit/dd3640548fe3d1e8b92784a63fac55d757dcc1f0)) +* **build:** validate installed dependencies before cache hits ([#982](https://github.com/morluto/rea/issues/982)) ([a45fe86](https://github.com/morluto/rea/commit/a45fe86a39b9e7a7c21e3665b6de4763cb72d782)) +* capture IndexedDB object-store record fingerprints ([#809](https://github.com/morluto/rea/issues/809)) ([21a8bd0](https://github.com/morluto/rea/commit/21a8bd0ccde2326c8bc022714a88a438d8b89dde)) +* **cli:** align structured output guard with native flags ([#777](https://github.com/morluto/rea/issues/777)) ([e9fac56](https://github.com/morluto/rea/commit/e9fac56232e74c8287863ca5c4cc7d8e39fb4a40)) +* **cli:** emit empty structured filter projections ([#914](https://github.com/morluto/rea/issues/914)) ([2db668f](https://github.com/morluto/rea/commit/2db668f6ca91d2514a354fd459239b84031812e7)) +* **cli:** honor the selected native plist path ([#773](https://github.com/morluto/rea/issues/773)) ([0374865](https://github.com/morluto/rea/commit/0374865836aa4960657fb92f588c71e35dd3bed8)) +* **cli:** preserve output and comparison input validation ([#981](https://github.com/morluto/rea/issues/981)) ([d5c43d6](https://github.com/morluto/rea/commit/d5c43d65f0f6a1de8fb2eb5f2c1940a6ea2af20c)) +* **cli:** preserve unsupported historical-source imports ([#795](https://github.com/morluto/rea/issues/795)) ([911d580](https://github.com/morluto/rea/commit/911d58063477912042da5e9e573a9e8008956622)) +* **contracts:** align CLI/MCP boundary contracts ([#946](https://github.com/morluto/rea/issues/946)) ([4a60738](https://github.com/morluto/rea/commit/4a6073810a955d058c10f209157cbd68832a92c5)) +* **deps:** remediate smol-toml advisories ([93091ab](https://github.com/morluto/rea/commit/93091abb4dbd3cceee217d179db816895df24d70)) +* **deps:** remediate smol-toml advisories ([c55ae3b](https://github.com/morluto/rea/commit/c55ae3b9eccc17e6e8f4369e13044995d0650704)) +* **dotnet:** compare members with undecoded signatures by exact identity ([28fd49f](https://github.com/morluto/rea/commit/28fd49f0f6cf54e373411f5999e6b5722dd826f5)) +* **dotnet:** compare members with undecoded signatures by exact identity ([d52af3b](https://github.com/morluto/rea/commit/d52af3b321a321964598fb302edeb26552c9dffd)) +* **dotnet:** inspect native boundaries of non-managed and malformed PEs ([d5f28fb](https://github.com/morluto/rea/commit/d5f28fb29c5c3e8acef940fcae042e660e3a4d72)) +* **dotnet:** inspect native boundaries of non-managed and malformed PEs ([d98dcfa](https://github.com/morluto/rea/commit/d98dcfa8c2e46a7cc918aeb01058c26c4a84060c)) +* **dotnet:** keep a decoded member unknown beside an undecoded counterpart ([76b7020](https://github.com/morluto/rea/commit/76b70203d941ab85c1772f330407fd832d438c9c)) +* **dotnet:** keep admitted CLI header facts when metadata is unreadable ([e5ba693](https://github.com/morluto/rea/commit/e5ba6934e4ff819c09077ca70bd2417d37f85bc0)) +* **dotnet:** name the shared key in exact-signature ambiguities ([7714e76](https://github.com/morluto/rea/commit/7714e763d9ac2e87994068b3dee37ca6c5ffc6f4)) +* **dotnet:** treat ambiguous undecoded members as possible counterparts ([73370e1](https://github.com/morluto/rea/commit/73370e1c064899753988581c9a821e0e7f579561)) +* **errors:** report union input issues at the union's path ([bddeb63](https://github.com/morluto/rea/commit/bddeb634b583cf4f7d5fa6cf3a1ee821e05d2923)) +* **evaluation:** canonicalize Unicode argument keys ([#980](https://github.com/morluto/rea/issues/980)) ([0a4caf4](https://github.com/morluto/rea/commit/0a4caf4a0a1355b6a121fc88886b22e55843ac76)) +* **firmware:** accept the verified Binwalk and Unblob release lines ([#844](https://github.com/morluto/rea/issues/844)) ([bef4321](https://github.com/morluto/rea/commit/bef4321afb3d0d480e8a04ad7e256663542caa3d)) +* **firmware:** retain multi-file extraction failures ([#978](https://github.com/morluto/rea/issues/978)) ([8aaf9a1](https://github.com/morluto/rea/commit/8aaf9a19788beb6612332505850ad3407cb00027)) +* **ghidra:** admit the 12.1 line and the install's JDK range ([#840](https://github.com/morluto/rea/issues/840)) ([5cbf06c](https://github.com/morluto/rea/commit/5cbf06c7ad0ace599721bffd7da566b8a790b622)) +* **ghidra:** select exact packaged bridge scripts ([#800](https://github.com/morluto/rea/issues/800)) ([894c30e](https://github.com/morluto/rea/commit/894c30ef02d946f0c8de3ae6c9168d9525d95033)) +* **ida:** share concurrent session cleanup ([#950](https://github.com/morluto/rea/issues/950)) ([f14b6a8](https://github.com/morluto/rea/commit/f14b6a84306116716e78c5ffc84353ac07939593)) +* **installer:** validate exact semantic versions ([#954](https://github.com/morluto/rea/issues/954)) ([248972f](https://github.com/morluto/rea/commit/248972f327776687037dc07099117584c0674e77)) +* integrate 24 reviewed browser/native/semantics corrections ([#815](https://github.com/morluto/rea/issues/815)) ([3af3811](https://github.com/morluto/rea/commit/3af3811ab0a212b6aab6185628d47ecad37958fc)) +* **javascript:** advertise the exact empty seed rule and match empty API keys ([35e5508](https://github.com/morluto/rea/commit/35e5508fd095a062d7477bd6bb7d2d8e28201cd8)) +* **javascript:** analyze source maps with empty source names ([58ef8ac](https://github.com/morluto/rea/commit/58ef8ac3aa803d7b64dd12a5b2a2e8ec24261060)) +* **javascript:** analyze sources containing empty string literals ([e7cbfe0](https://github.com/morluto/rea/commit/e7cbfe04fdfd7affe50dba9e2c7a9a0a72291041)) +* **javascript:** analyze sources containing empty string literals ([11fc3b9](https://github.com/morluto/rea/commit/11fc3b981509e8107b10b2e75363f5bb489879db)) +* **javascript:** filter keyed-collection receivers only for get/delete ([00f707b](https://github.com/morluto/rea/commit/00f707b250451246d69a1205c39c6843fab6bb5b)) +* **javascript:** handle deep AST traversal and addition without stack overflow ([#915](https://github.com/morluto/rea/issues/915)) ([62b3711](https://github.com/morluto/rea/commit/62b37111e7f09887eb712c6e8e721e0334cf10b9)) +* **javascript:** handle deep static member paths ([#939](https://github.com/morluto/rea/issues/939)) ([348c65c](https://github.com/morluto/rea/commit/348c65c7ece1aee280f9431d995996b444e7549d)) +* **javascript:** keep bare relative URLs for computed-method XHR ([e32028d](https://github.com/morluto/rea/commit/e32028d1b566872ba8cd08fc6800c198d6aef30b)) +* **javascript:** keep computed-method XHR request URLs ([602d002](https://github.com/morluto/rea/commit/602d0021d45a6cdab9f806554f3ee12ea670ec97)) +* **javascript:** keep exact empty env keys and method names ([522c2d0](https://github.com/morluto/rea/commit/522c2d0ecb1eb7be262eaef2926cceac9a68badc)) +* **javascript:** keep exact empty env keys and method names ([f7954a8](https://github.com/morluto/rea/commit/f7954a8fd692f262ef3ce1507b957681150ce9c4)), closes [#783](https://github.com/morluto/rea/issues/783) +* **javascript:** keep exact empty require paths and request fields ([ad1f6b5](https://github.com/morluto/rea/commit/ad1f6b508fbdd44d8bf1884ed96ae69082808e7d)), closes [#783](https://github.com/morluto/rea/issues/783) +* **javascript:** keep exact empty values in graph identities ([f608127](https://github.com/morluto/rea/commit/f608127635ed8e0d105ad95252e5ea0dff899f5b)) +* **javascript:** keep exact empty values in graph identities ([63012ee](https://github.com/morluto/rea/commit/63012eeb601fc74ca82f3a027d852a5da1b099d2)) +* **javascript:** keep keyed-collection reads out of endpoints ([9075ce7](https://github.com/morluto/rea/commit/9075ce71ada4334407aa2d194dc82e3031e30260)) +* **javascript:** keep keyed-collection reads out of endpoints ([eb68761](https://github.com/morluto/rea/commit/eb68761eb0a35b876e33dd6115d0d07f97e99dd5)) +* **javascript:** keep literal HTTP methods on storage-named receivers ([9f48a76](https://github.com/morluto/rea/commit/9f48a76e1186cc43c6928eff1c82e93280caee1a)) +* **javascript:** keep literal-method and URL-literal requests on aliased receivers ([e3e7664](https://github.com/morluto/rea/commit/e3e76646657e950e24241ba715eb97ea385933e9)) +* **javascript:** keep mutated object initializer values unknown ([#752](https://github.com/morluto/rea/issues/752)) ([6caa38a](https://github.com/morluto/rea/commit/6caa38a0cad369ddff52b08f8d7d44c1b92ecac0)) +* **javascript:** keep storage open versions out of XHR endpoints ([4f51103](https://github.com/morluto/rea/commit/4f5110306d9edab94bf63bf81cf259324d96af57)) +* **javascript:** keep storage open versions out of XHR endpoints ([898b0f6](https://github.com/morluto/rea/commit/898b0f62d76ea1cf793140cb255cad16ab85b44d)) +* **javascript:** keep XHR and client endpoints that receiver spellings hid ([05f43ed](https://github.com/morluto/rea/commit/05f43ed388ae4e9316d320b592b6eba5b119ce89)) +* **javascript:** leave empty application values unlabeled ([527b76e](https://github.com/morluto/rea/commit/527b76ef78e84d29bb41bd45f1f83fae05ca66ca)) +* **javascript:** match exact empty literal channels and seeds ([e20da1f](https://github.com/morluto/rea/commit/e20da1f205b105b1b6a5decd3c2d0fe36fd12410)) +* **javascript:** never read window or document open as XHR ([ae849a7](https://github.com/morluto/rea/commit/ae849a72811857ba9755962ec48a1d76c9d1181a)) +* **javascript:** preserve computed Electron member uncertainty ([3a4a63f](https://github.com/morluto/rea/commit/3a4a63f70a2e42fb1fa48881e08b08d612289f2e)) +* **javascript:** preserve computed Electron member uncertainty ([1260cac](https://github.com/morluto/rea/commit/1260cacc67b8e0c93fb9434452053feca956e7e7)) +* **javascript:** project decoded file URL paths ([#810](https://github.com/morluto/rea/issues/810)) ([2572e1e](https://github.com/morluto/rea/commit/2572e1eb2f853a4cebfc8303d9e293e1d106d608)) +* **javascript:** read open() endpoints only from XHR method/URL pairs ([63e0822](https://github.com/morluto/rea/commit/63e0822cc794bd6ec1688340b3230ffb6b5c4a8c)) +* **javascript:** read open() endpoints only from XHR method/URL pairs ([1be3705](https://github.com/morluto/rea/commit/1be370543f7ea39016f86ebdec80a8ceaf7f8a28)) +* **javascript:** read substitution-free templates as exact strings ([c371d6e](https://github.com/morluto/rea/commit/c371d6e1e9cceb28e6cfc874e14c4e740bb87751)) +* **javascript:** read substitution-free templates as exact strings ([77cf757](https://github.com/morluto/rea/commit/77cf75710369acc6c304ec352ab25aa7274e04b1)) +* **javascript:** resolve bare source-map and worker URLs next to the script ([#890](https://github.com/morluto/rea/issues/890)) ([45fcd28](https://github.com/morluto/rea/commit/45fcd28b4018723bdb508a9229b74ab9eb749f75)) +* **javascript:** retain recovered call argument positions ([#929](https://github.com/morluto/rea/issues/929)) ([e6ba9c3](https://github.com/morluto/rea/commit/e6ba9c3af3053485f154825eea6a156127328be9)) +* **linux:** improve Hopper reliability and CachyOS support ([#963](https://github.com/morluto/rea/issues/963)) ([98b2b02](https://github.com/morluto/rea/commit/98b2b027afd8c80b68e9daaf97bdaa4d942105cc)) +* **mcp:** advertise reserved process environment key ([#949](https://github.com/morluto/rea/issues/949)) ([e0d76ae](https://github.com/morluto/rea/commit/e0d76ae80ddc70ae4d5afcbd1427afb81eee3669)) +* **mcp:** isolate optional observation adapter failures ([#838](https://github.com/morluto/rea/issues/838)) ([be3cfbf](https://github.com/morluto/rea/commit/be3cfbffdfbbb65da75dcec0518e2ebf387e66bc)) +* **mcp:** preserve exact guided prompt context ([#761](https://github.com/morluto/rea/issues/761)) ([66c26f2](https://github.com/morluto/rea/commit/66c26f280b1d494806ee182b52564166760f0ae2)) +* **native-ui:** handle incomplete accessibility child collections ([#944](https://github.com/morluto/rea/issues/944)) ([ac3bae3](https://github.com/morluto/rea/commit/ac3bae3cb97c4fb622eb1b6ac1f3b84e6a70ba03)) +* **native:** inspect plists containing data, dates, and non-finite reals ([bc2cd8b](https://github.com/morluto/rea/commit/bc2cd8b874e115eee446860043758a80bd583ad0)) +* **native:** inspect plists containing data, dates, and non-finite reals ([5ead2cf](https://github.com/morluto/rea/commit/5ead2cfbc47896b43502363d3568330f5d94762e)) +* **native:** keep echoed codesign paths out of signature fields ([#865](https://github.com/morluto/rea/issues/865)) ([4aa2b60](https://github.com/morluto/rea/commit/4aa2b60c1d84607a138395ae93260b2e50c2a721)) +* **native:** keep echoed Mach-O paths out of load commands and symbols ([ab985c6](https://github.com/morluto/rea/commit/ab985c6a10e9da856ed151100cee8459793c3573)) +* **native:** keep echoed Mach-O paths out of load commands and symbols ([37fe105](https://github.com/morluto/rea/commit/37fe105ba168488d05226f2912bed802e2d48d75)) +* **native:** keep integral plist reals beyond the JSON range as reals ([#860](https://github.com/morluto/rea/issues/860)) ([3b92a2b](https://github.com/morluto/rea/commit/3b92a2bceddaa1659654da08160813a3affbc6b7)) +* **native:** keep plist XML element types for large numbers ([f85056d](https://github.com/morluto/rea/commit/f85056d83e42a461c8c947792774580294d0ceb9)) +* **native:** keep the resolved bundle plist and permission failures ([418a556](https://github.com/morluto/rea/commit/418a5562f6f640503c751924a4787586c2c5c98a)) +* **native:** pass Swift symbols to swift-demangle as operands ([dd90965](https://github.com/morluto/rea/commit/dd90965058a9e6f7dd7f30eb7e462572e777db7f)) +* **native:** pass Swift symbols to swift-demangle as operands ([6c88094](https://github.com/morluto/rea/commit/6c88094da45258b346fe594764e561f999622a58)) +* **native:** preserve incomplete native UI captures ([#991](https://github.com/morluto/rea/issues/991)) ([9f496ef](https://github.com/morluto/rea/commit/9f496ef2d4a3aa68816287bf5d2ba1620d72c97a)) +* **native:** report exact plist integers beyond the JSON number range ([c9f49db](https://github.com/morluto/rea/commit/c9f49db907db0513ad8b22c9b2bbdcee400aff11)) +* **native:** report exact plist integers beyond the JSON number range ([430c297](https://github.com/morluto/rea/commit/430c297ca4ddb3491c2b835b889486e6a2ffdc17)) +* **native:** report plist permission denials as access failures ([5738a0c](https://github.com/morluto/rea/commit/5738a0cf8c111833847fecb2f7d0c2ee35eabbd6)) +* **native:** report why a selected plist cannot be inspected ([14f9b3d](https://github.com/morluto/rea/commit/14f9b3d93119a60a3cf5f3d9c138ca829a5462e8)) +* **native:** report why a selected plist cannot be inspected ([0674fac](https://github.com/morluto/rea/commit/0674facd468c3321584ece908031feee7220b9c6)) +* **plist:** keep character references beyond Unicode literal ([5afd8a8](https://github.com/morluto/rea/commit/5afd8a850b2ebd62e3400a5e2ece1a1cfa98bab6)) +* **plist:** read XML plists containing a __proto__ key ([faaee69](https://github.com/morluto/rea/commit/faaee693d5bb4bb957387ebd0f4338513c4c914a)) +* **plist:** read XML plists containing a __proto__ key ([a08be51](https://github.com/morluto/rea/commit/a08be51e9f0d23bd46b66bbdb88dbda44bc54c88)) +* **plist:** report omitted __proto__ entries in signatures and Interface Builder ([15ab5db](https://github.com/morluto/rea/commit/15ab5dbcaf3fac09756e98926373965247dad622)) +* **plist:** report omitted __proto__ entries in signatures and Interface Builder ([6729ee7](https://github.com/morluto/rea/commit/6729ee7636889a0a2ebc9fca8e35860476f8ca34)) +* preserve native UI failure reasons and dedupe version literals ([#854](https://github.com/morluto/rea/issues/854)) ([2d4a910](https://github.com/morluto/rea/commit/2d4a910fcd30e8d5b96fcb957b931e9aa3e4ab0c)) +* **process:** clarify Windows capture support and recovery ([#798](https://github.com/morluto/rea/issues/798)) ([50787b2](https://github.com/morluto/rea/commit/50787b2faf1a4bea799d041960ca2b91199ef403)) +* **process:** harden filesystem snapshot identity and cancellation ([#956](https://github.com/morluto/rea/issues/956)) ([93aa9eb](https://github.com/morluto/rea/commit/93aa9eb5c2551984182a4025d836e4223e8ecc8a)) +* **process:** retain provider output until streams close ([#928](https://github.com/morluto/rea/issues/928)) ([b929251](https://github.com/morluto/rea/commit/b9292516f4eedc2dedba713abf57f665ef1fb596)) +* **reference:** retain partial imports for unreadable directories ([#766](https://github.com/morluto/rea/issues/766)) ([cffc548](https://github.com/morluto/rea/commit/cffc548cd3127c5db811f20db4816039846b3170)) +* **release:** drive checkpoint releases from frozen branches ([fc58fa1](https://github.com/morluto/rea/commit/fc58fa1eb01fe5a8200a78e9551799c9c1a3d256)) +* **release:** publish from explicit source checkpoints ([49887f0](https://github.com/morluto/rea/commit/49887f0239da4483153a5e1610058619855525db)) +* **semantics:** isolate named function expression bindings ([#776](https://github.com/morluto/rea/issues/776)) ([ae81f64](https://github.com/morluto/rea/commit/ae81f64d2ad10e5cdbf8356819351c02480aee7a)) +* **semantics:** preserve direct promise ownership boundaries ([#758](https://github.com/morluto/rea/issues/758)) ([e1ca412](https://github.com/morluto/rea/commit/e1ca412757bb01be855a66bf72006e3becd497b6)) +* **semantics:** preserve empty string property keys ([#764](https://github.com/morluto/rea/issues/764)) ([630bdaf](https://github.com/morluto/rea/commit/630bdafcacc621105f6a43ed68a6e51651aca629)) +* **semantics:** recognize Node builtin default namespaces ([#769](https://github.com/morluto/rea/issues/769)) ([24a0bf9](https://github.com/morluto/rea/commit/24a0bf9a618604e2fa30817d673ce911c7842819)) +* **semantics:** retain trailing rest argument flows ([#756](https://github.com/morluto/rea/issues/756)) ([4bfd803](https://github.com/morluto/rea/commit/4bfd8038be0a60c43a452bec15e23799a0b040c0)) +* **setup:** accept the pinned Hopper RPM launcher ([#933](https://github.com/morluto/rea/issues/933)) ([dda8e6a](https://github.com/morluto/rea/commit/dda8e6a72890f87bdf8bbd3250b68b6648aae553)) +* **setup:** read an OpenCode V2 server named type as a table entry ([#866](https://github.com/morluto/rea/issues/866)) ([4e17be5](https://github.com/morluto/rea/commit/4e17be57053fb0ce2561d92e05b6e38ef789b2d4)) +* **setup:** register OpenCode V2 configurations in mcp.servers ([957069d](https://github.com/morluto/rea/commit/957069d14d832ddeac3f90dd141d379b81e2832a)) +* **setup:** register OpenCode V2 configurations in mcp.servers ([69d8afe](https://github.com/morluto/rea/commit/69d8afefc19e014a4bba7a182983a9e77e100a75)) +* **target:** decode app Info.plist executables with an XML parser ([c57f1b7](https://github.com/morluto/rea/commit/c57f1b74e0af5cf5fbb459cb247e1448cbf8df1b)) +* **target:** decode app Info.plist executables with an XML parser ([c182f72](https://github.com/morluto/rea/commit/c182f72ad3d9eea10b084a43527acb7c8ca2d6bf)) +* **test:** disconnect Hopper fixture IPC on SIGTERM ([b912b11](https://github.com/morluto/rea/commit/b912b117f019d5d3e4b4e3ef683e9b25f3a709bc)) +* **windows:** accept ordinary drive path separators ([#791](https://github.com/morluto/rea/issues/791)) ([b62931f](https://github.com/morluto/rea/commit/b62931f8335f58bfe344883a4f149e9eef73d835)) +* **windows:** remove cancelled snapshot outputs ([#957](https://github.com/morluto/rea/issues/957)) ([d76f7d1](https://github.com/morluto/rea/commit/d76f7d150895289aa1fb85a057b81ca7ff102df4)) + + +### Performance Improvements + +* **artifacts:** stream ASAR members without full buffering ([#962](https://github.com/morluto/rea/issues/962)) ([abd7a5e](https://github.com/morluto/rea/commit/abd7a5ed091061e4b1e83e5933e9009fb5b8d577)) +* **call-path:** prune dead ends and enumerate without recursion ([#909](https://github.com/morluto/rea/issues/909)) ([e52fe97](https://github.com/morluto/rea/commit/e52fe9719fec03f2aa79b1a1b9e11b38c229903c)) +* **cli:** defer catalog digests and use canonical cache scope ([9a15525](https://github.com/morluto/rea/commit/9a1552529889e9f5b52a88739eb767b92e57e24e)) +* **cli:** defer unused catalog work during command startup ([#920](https://github.com/morluto/rea/issues/920)) ([9a15525](https://github.com/morluto/rea/commit/9a1552529889e9f5b52a88739eb767b92e57e24e)) +* **comparison:** reuse canonical sort keys ([#990](https://github.com/morluto/rea/issues/990)) ([ebb065e](https://github.com/morluto/rea/commit/ebb065e2dc15f5763276f401521896f66bae6339)) +* **dotnet:** index declaring-type ranges once per metadata table ([#918](https://github.com/morluto/rea/issues/918)) ([7a9f587](https://github.com/morluto/rea/commit/7a9f587c3a9998d828b83388438976527ca7074f)) +* **dotnet:** index ordered declaring-type ranges once per table ([7a9f587](https://github.com/morluto/rea/commit/7a9f587c3a9998d828b83388438976527ca7074f)) +* **evidence:** scale imports and dependency graphs ([#941](https://github.com/morluto/rea/issues/941)) ([7dc40d3](https://github.com/morluto/rea/commit/7dc40d3d9e00caf013109942a5d9a1d7fdf9ec62)) +* **inspector:** bound runtime script location authorization ([#945](https://github.com/morluto/rea/issues/945)) ([58381ee](https://github.com/morluto/rea/commit/58381eefa24f73580e31eaa09dc14b9c04d78d6c)) +* **javascript:** analyze member chains without recursive copying ([#932](https://github.com/morluto/rea/issues/932)) ([dcde958](https://github.com/morluto/rea/commit/dcde9584508830a5d99ac5e2032f539563c69002)) +* **javascript:** index Promise return-site ownership ([#971](https://github.com/morluto/rea/issues/971)) ([2cedde9](https://github.com/morluto/rea/commit/2cedde9f1cf72b04bef37f9f004bb68d5ff6502a)) +* **javascript:** index semantic fingerprint relations ([#942](https://github.com/morluto/rea/issues/942)) ([c92a344](https://github.com/morluto/rea/commit/c92a344d4770125d85a11f3fc7a23d800bd30003)) +* **javascript:** reduce analysis and source-map work ([#955](https://github.com/morluto/rea/issues/955)) ([c7530f9](https://github.com/morluto/rea/commit/c7530f9a816d84354516d3ea137f4a0d91d3b8b3)) +* **providers:** assemble fragmented responses once per line ([#911](https://github.com/morluto/rea/issues/911)) ([df7151b](https://github.com/morluto/rea/commit/df7151ba305eab3a33a9261a023705b29862b308)) +* **reconstruction:** index coverage evaluation ([#983](https://github.com/morluto/rea/issues/983)) ([f26c8b5](https://github.com/morluto/rea/commit/f26c8b5b8cff10b93173359b5453874d659f2440)) +* **setup:** reuse initial diagnostics during planning ([#940](https://github.com/morluto/rea/issues/940)) ([118d43e](https://github.com/morluto/rea/commit/118d43e787e38857714852cc846b1b604a61950d)) + + +### Code Refactoring + +* add incremental capability migration safeguards ([#819](https://github.com/morluto/rea/issues/819)) ([75f50d8](https://github.com/morluto/rea/commit/75f50d86f6d504f4dd97a707871d39ec022b3047)) +* **android:** group layered source and verification ([#873](https://github.com/morluto/rea/issues/873)) ([8e8921d](https://github.com/morluto/rea/commit/8e8921d40c63b40ab7e1598f672063dd304d93f0)) +* **apple:** group artifact producers and verification ([#952](https://github.com/morluto/rea/issues/952)) ([ef1a0cc](https://github.com/morluto/rea/commit/ef1a0cc65af7918ef7204a39f42a4299b486c453)) +* **artifacts:** isolate JavaScript file acquisition ([#916](https://github.com/morluto/rea/issues/916)) ([f0a68dc](https://github.com/morluto/rea/commit/f0a68dce60e81b4d2f420bf774b61957a37326ac)) +* **binary:** group session selection and snapshot ownership ([#886](https://github.com/morluto/rea/issues/886)) ([dd114d3](https://github.com/morluto/rea/commit/dd114d39801973e958a582c8fcba7ccce0e50299)) +* **binary:** move production composition outside application workflows ([#965](https://github.com/morluto/rea/issues/965)) ([33d4761](https://github.com/morluto/rea/commit/33d4761cc7a96630b7c2fed890df83d433c45914)) +* **browser:** align inspector exclusions with canonical vocabulary ([#834](https://github.com/morluto/rea/issues/834)) ([de9593a](https://github.com/morluto/rea/commit/de9593ad48c0b97507f18949b7ad86ad9c743ebc)) +* **cli:** split core analysis command registrar ([#858](https://github.com/morluto/rea/issues/858)) ([8b9940a](https://github.com/morluto/rea/commit/8b9940a7b5d3fcd5a8eda6d397f0ab7c3aaf2ac9)) +* colocate investigation record implementation ([#867](https://github.com/morluto/rea/issues/867)) ([bb19c94](https://github.com/morluto/rea/commit/bb19c9477ef3eefd9c5b2e6cabe78cc94e6779ae)) +* compose investigation record ownership ([#845](https://github.com/morluto/rea/issues/845)) ([ce73c3f](https://github.com/morluto/rea/commit/ce73c3fed2e5fd20ae0113579345a69763eb7655)) +* declare auxiliary capabilities from source ([#853](https://github.com/morluto/rea/issues/853)) ([4a6c17c](https://github.com/morluto/rea/commit/4a6c17ce83dfd20412f0e168525314a20c61169e)) +* **domain:** consolidate property-key extraction behind semanticStaticPropertyKey ([#823](https://github.com/morluto/rea/issues/823)) ([764ff05](https://github.com/morluto/rea/commit/764ff05231f1a666dfa655492da42b4b6ba742c3)) +* **domain:** converge comparison mechanics on shared vocabulary ([#824](https://github.com/morluto/rea/issues/824)) ([c13c9d4](https://github.com/morluto/rea/commit/c13c9d41f8441a711266adee21429fb7433a6aa1)) +* **domain:** extract artifact path syntax helpers ([#830](https://github.com/morluto/rea/issues/830)) ([ef535b8](https://github.com/morluto/rea/commit/ef535b81fefc358b0bdb655e5aa057fc3949ce5f)) +* **domain:** extract source-map envelope walk and disambiguate stringValue ([#828](https://github.com/morluto/rea/issues/828)) ([64ebdf4](https://github.com/morluto/rea/commit/64ebdf45016bda679674cbd2287576eadf66a1fc)) +* **domain:** map validation taxonomies and add inventory Result seam ([#833](https://github.com/morluto/rea/issues/833)) ([78f4848](https://github.com/morluto/rea/commit/78f4848d70ce462426e9f221d6e7b3e34219bf46)) +* **domain:** split Interface Builder keyed-archive parse from graph builder ([#859](https://github.com/morluto/rea/issues/859)) ([da09706](https://github.com/morluto/rea/commit/da0970672da725dbb095ffcaefad07303ae85aab)) +* **firmware:** group layered source and verification ([#878](https://github.com/morluto/rea/issues/878)) ([507e8b3](https://github.com/morluto/rea/commit/507e8b3c7be75f8de69be214618394aca3585404)) +* **ghidra:** extract createClient and load-image attestation ([#856](https://github.com/morluto/rea/issues/856)) ([b7df5b7](https://github.com/morluto/rea/commit/b7df5b78052539e57967db8e6562befd70c6ebdc)) +* give Inspector its own capability directory ([#869](https://github.com/morluto/rea/issues/869)) ([021ba69](https://github.com/morluto/rea/commit/021ba690b07eab4b990be47f90946db9f70e0fc0)) +* **javascript:** group application workflows and verification ([#896](https://github.com/morluto/rea/issues/896)) ([0804722](https://github.com/morluto/rea/commit/0804722cdf721a8d767d02f9342ebdf8d186f7e1)) +* **javascript:** group domain semantics and contracts ([#907](https://github.com/morluto/rea/issues/907)) ([f6a58f1](https://github.com/morluto/rea/commit/f6a58f10629614fb7a6970421218b73d41bce6a5)) +* **managed:** group capability workflows and verification ([#926](https://github.com/morluto/rea/issues/926)) ([28713f9](https://github.com/morluto/rea/commit/28713f9b4dcb882f0e811653890fde70cb5fdadb)) +* **native:** group analyst semantics workflows and contracts ([#973](https://github.com/morluto/rea/issues/973)) ([ea4db4c](https://github.com/morluto/rea/commit/ea4db4c9f7cbc10dc701c4d5770109c08952a5b8)) +* **native:** split Apple dispatch metadata decode facets ([#855](https://github.com/morluto/rea/issues/855)) ([676959e](https://github.com/morluto/rea/commit/676959ecb55b606a567af3d2eb3d3edc96f0aa2f)) +* **provider:** declare deep capabilities from source ([#892](https://github.com/morluto/rea/issues/892)) ([b808e2d](https://github.com/morluto/rea/commit/b808e2d6bbbae38c1ff2e293e1cf77464d889bdd)) +* share Android and firmware provider factories ([#826](https://github.com/morluto/rea/issues/826)) ([3ed59b2](https://github.com/morluto/rea/commit/3ed59b281247f2180b3c79e658078507e4cc1be2)) + + +### Documentation + +* add concise lawful-use disclaimer ([#888](https://github.com/morluto/rea/issues/888)) ([4037c65](https://github.com/morluto/rea/commit/4037c6545dc8cc47f4602d8f06e5329ed641cdbb)) +* add DX-Ball reconstruction showcase ([#876](https://github.com/morluto/rea/issues/876)) ([895079d](https://github.com/morluto/rea/commit/895079dd093f02487789d9aa967f0cccbfe379d8)) +* add prominent website and showcase links to READMEs ([2ccc86c](https://github.com/morluto/rea/commit/2ccc86cd13d71f08b553d3b9962175144b882539)) +* add README star history chart ([#850](https://github.com/morluto/rea/issues/850)) ([11f27de](https://github.com/morluto/rea/commit/11f27deddf88703368942d3ddfc239a7ebe72e3e)) +* add website and showcase links to READMEs ([#930](https://github.com/morluto/rea/issues/930)) ([2ccc86c](https://github.com/morluto/rea/commit/2ccc86cd13d71f08b553d3b9962175144b882539)) +* **apple:** describe directory-based framework plist ambiguity ([6d74992](https://github.com/morluto/rea/commit/6d74992efed3271e8370c505afdde347609f8353)) +* audit and improve localized readmes translations and wording ([00a6ac3](https://github.com/morluto/rea/commit/00a6ac3de99f432bd6dbb88148a2aab5f8d3d338)) +* clarify NativeAOT scope and regenerate conformance ledger ([c945931](https://github.com/morluto/rea/commit/c945931961cbd488163c6d92ef91336ed506942e)) +* fix stale IDA roadmap item, mermaid paths, toolchain wording ([d2aed17](https://github.com/morluto/rea/commit/d2aed172ab79d98ff4e62c14d8f34cc49c1d17b4)) +* move quick start up and mark skill install optional ([554e9b1](https://github.com/morluto/rea/commit/554e9b1612f7799c2bc7177eb1a9b04a0f27d2e9)) +* note Cloud Agent Node.js toolchain ([#837](https://github.com/morluto/rea/issues/837)) ([6aa6350](https://github.com/morluto/rea/commit/6aa6350c800796d0953937e894ff176ae248e930)) +* publish VitePress documentation on GitHub Pages ([c78fe69](https://github.com/morluto/rea/commit/c78fe6990bcface6853ac5c11c90df6bc0df562d)) +* separate ambiguous and unavailable provider recovery ([c3f2b8e](https://github.com/morluto/rea/commit/c3f2b8e7742915dc27ab7c801a8a1a9127169896)) +* show task-scoped readiness and provider preference in the quick start ([880cca1](https://github.com/morluto/rea/commit/880cca1bca497bb53c97a821b470df0f2235e043)) +* show task-scoped readiness and provider preference in the quick start ([5fd0db3](https://github.com/morluto/rea/commit/5fd0db37d94f1c1a0495920c79e121d799da0d30)), closes [#726](https://github.com/morluto/rea/issues/726) +* update release boundary to npm 4.1.0 with Windows native bundle ([69be514](https://github.com/morluto/rea/commit/69be514c67c8b2eb8b450355a9782d9237748fda)) +* **website:** add Notion case study and improve onboarding ([a6ba772](https://github.com/morluto/rea/commit/a6ba772c08e2648e6602787a0ea537d75d72802d)) +* **website:** add TH04 bullet-ring case study ([f8d84da](https://github.com/morluto/rea/commit/f8d84da127fad871050c098b004a503b36795ad0)) +* **website:** add worked guides and keep Pages publishing manual ([#925](https://github.com/morluto/rea/issues/925)) ([4f25ee3](https://github.com/morluto/rea/commit/4f25ee33dcd4195f8d877f8835f81c4e08c95235)) +* **website:** clarify guides and case studies ([5065fcd](https://github.com/morluto/rea/commit/5065fcd5205ec7c0d259131431c2cf54d9f69cf6)) + + +### Tests + +* **artifacts:** report compiled connector records when a storyboard route is missing ([d421d3f](https://github.com/morluto/rea/commit/d421d3f286ce515b5126859a0f9cac60667a168e)) +* **artifacts:** report observed action routes when a compiled route is missing ([c60b737](https://github.com/morluto/rea/commit/c60b737f7db8e2c9a1982cdb4357fcda1f47ff08)) +* **artifacts:** write the storyboard fixture's action in Cocoa syntax ([53bfa7e](https://github.com/morluto/rea/commit/53bfa7e3f792cdb9f2e95fc954f3616426414bcc)) +* eliminate fixture races and prune redundant scaffolding ([#849](https://github.com/morluto/rea/issues/849)) ([f72931d](https://github.com/morluto/rea/commit/f72931d9dd54609bfca422750fc2667f80af691c)) + + +### Continuous Integration + +* consolidate macOS native baseline checks ([#935](https://github.com/morluto/rea/issues/935)) ([810fe53](https://github.com/morluto/rea/commit/810fe53e799b89dc215c52c9245d36fb2a44d9c4)) +* verify native arm64 and Intel macOS workflows ([#898](https://github.com/morluto/rea/issues/898)) ([4801a24](https://github.com/morluto/rea/commit/4801a24089964860db7f4855be21d2d4313c7eab)) + ## [4.1.0](https://github.com/morluto/rea/compare/rea-agents-4.0.1...rea-agents-4.1.0) (2026-10-06) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07b182bd24..ca5fff69df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ When adding or changing an MCP tool, follow the [tool design guide](docs/tool-de ## Development setup -REA development requires Node.js 24.18.x and npm 11.16.x (pinned toolchain via `nvm use`; the supported runtime range is Node.js ^22.19 || ^24.11 || >=26, as the README badge states). Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, or 64-bit Arch) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies. +REA development requires Node.js 24.18.x and npm 11.16.x (pinned toolchain via `nvm use`; the supported runtime range is Node.js ^22.19 || ^24.11 || >=26, as the README badge states). Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, 64-bit Arch, or CachyOS) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies. ```bash npm ci @@ -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 -. 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. +use `npm run docs:generate`; `docs:build` generates them before building 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 + 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 @@ -72,8 +74,24 @@ require the matching real-provider `verify:*` lane. Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use the same configuration. Pre-commit formats and lints staged files; pre-push runs `check:fast`. -`docs:check` checks committed generated metadata. `docs:generate` regenerates -those files, and the docs CI lane checks them. +`docs:check` builds and validates generated metadata for the current checkout. +The product catalog (`docs/public/product-catalog.json`), portable managed +conformance projections (`docs/verification/managed-conformance-*.json`), and +packaged skill (`skills/`) are ignored build outputs. Edit skill instructions +and references in `skill-src/`; the build adds catalog-dependent metadata to +the packaged copy without rewriting authored files. The generated manifest +commits to that exact packaged skill bundle. It is a portable projection of +the deterministic managed verifier, not a record of optional real-provider runs. +CI validates these outputs and retains them as artifacts instead of pushing +generated-only commits onto feature branches. Reviewed source metadata such as +`src/generatedPackageMetadata.ts` and `docs/error-contract.schema.json` remains +tracked and checked for freshness. Do not commit ignored generated outputs. +The build-generated product catalog contains documented facts and their provider +identity, rather than full runtime schema hashes. The managed skill contains +instructions and inventory metadata; doctor compares its installed files with +the canonical bundle. Schema-only fixes should not change these outputs or the +skill commitment in the conformance manifest. Runtime schema identity remains +available through doctor and `binary_session`. Real-provider execution remains uncached; deterministic builds use Turbo. Local `npm test` runs every deterministic Vitest project without coverage or @@ -120,32 +138,14 @@ Describe the behavior change and verification performed in the pull request. Nev ## Maintainer release checklist -Run `npm run check:pr`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary: +Use the [checkpoint release guide](docs/releasing.md). Releases start from an +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. -```bash -npm pack -``` - -Use the exact filename printed by `npm pack` to run the packaged executable: - -```bash -npm exec --yes --package ./rea-agents-VERSION.tgz -- rea --help -``` - -Replace `VERSION` with the packed version; do not use a tarball from an earlier -build. - -Publish the public package: - -```bash -npm publish --access public -``` - -After npm registry propagation, verify the published CLI and connect the client SDK version pinned in `package.json` to the published server to confirm the canonical tool catalog: - -```bash -npx -y rea-agents@latest --help -npx -y rea-agents@latest doctor -npx -y rea-agents@latest setup --yes --all-detected -npx -y rea-agents@latest mcp -``` +Keep new implementation commits on main for the next release. The workflow +owns packaged-artifact verification, npm publication, the published CLI/MCP +canary, and MCP Registry publication. Sync release metadata back to main after +publication; see the guide for partial-publication recovery and verification. diff --git a/README.md b/README.md index 9419a1b860..94800af9c3 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,21 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](#tool-catalog-for-investigation) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) +[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) [![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM) +🎉 **20,000 GitHub stars — thank you!** + +Thanks to everyone using REA, reporting bugs, testing builds, and contributing fixes. + morluto%2Frea | Trendshift -**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [DX-Ball showcase](https://morluto.github.io/rea/showcase/dx-ball/)** +**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [Showcases](https://morluto.github.io/rea/showcase/)** -[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works) +[Quick start](#quick-start) · [How REA works](#how-rea-works) · [What you can analyze](#what-you-can-analyze) · [Showcases](#showcases) · [FAQ](#faq) · [Documentation](#documentation) npx rea-agents setup @@ -53,760 +58,266 @@ REA connects your agent to tools for inspecting native binaries, JavaScript and Setup registers REA with your agent and installs matching workflow instructions. Native analysis can use an existing Hopper or Ghidra installation; setup can optionally install Hopper with approval. Static JavaScript analysis needs neither engine. +> **[Visit the REA website](https://morluto.github.io/rea/)** for setup instructions, illustrated guides, and real case studies. + ## Quick start -### Run setup (recommended) +### Set up your agent -Set up REA with your agent: +With Node.js and npm installed, run: ```bash npx rea-agents setup ``` -Choose which supported agents should use REA, then review the exact paths and changes before approving. Existing REA registrations are selected by default; newly detected agents are available to select, but detection alone does not select them. Setup adds MCP access and REA's guided workflow for selected agents. Hopper is a separate optional choice with its own consent. Setup can also record an existing Ghidra installation. +Choose your agents, review the proposed changes, and approve them. Setup adds +REA's MCP server and matching workflow instructions, with backups of existing +configuration. Restart your agent afterward. -Setup shows its changes before applying them and backs up existing configuration. See [Installation and setup](docs/installation.md) for requirements and setup options. +Setup supports Claude Code, Codex, Cursor, Gemini CLI and +[other agents](docs/installation.md#supported-agents). See +[installation and setup](docs/installation.md) for provider configuration and +manual MCP registration. -### AI Coding Assistants (optional) - -Add the skill to your AI coding assistant for richer context: - -```bash -npx skills add morluto/rea --skill reverse-engineer-anything -``` - -The skill provides REA's investigation workflow. Run setup above to connect REA to your agent and configure analysis tools. Setup already installs a version-matched skill by default; this command installs the repository version. - -### With an agent (recommended) - -After setup, restart your agent and [describe the app or feature](#just-ask-your-agent) you want to understand. Hopper can run in demo mode; if it shows a first-run prompt, choose the demo or enter an existing license. - -REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration). - -### First result from the terminal - -For your extracted JavaScript/Electron application tree or ASAR, run: - -```bash -npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json -``` - -Replace the path with your target (for example, `"D:/apps/example"` on Windows). -This returns inline Evidence, recovered graph, limitations, and unknowns without -MCP setup, Hopper, Ghidra, or executing the application. For a native app, configure -its engine first, then use `analyze` with that app's path. Run `doctor` when you -need diagnosis; it is not a prerequisite for each analysis. - -### Check readiness for the task at hand - -`rea doctor` without options is an audit of the whole integration. It checks -every detected agent registration, the installed skill, and every optional -analysis engine, so it can report `healthy: false` while your current task works. -Choose a readiness scope for the work you are doing: - -| Task | Readiness check | -| ----------------------------------- | -------------------------------------------------------------------------------------------- | -| Static JavaScript/Electron analysis | None. Run `analyze-javascript-application` directly. | -| Troubleshoot one analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`) | -| Check one agent's MCP registration | `rea doctor --client codex --json` (see [client IDs](docs/installation.md#supported-agents)) | -| Check the installed skill | `rea doctor --skill --json` | - -A scoped report has `scope.mode: "explicit"`. Only `scope_checks` determine -`healthy` and the exit status. Everything else is listed in -`informational_checks`; you don't need to fix it for this task. For example, a -missing engine you are not using needs no repair. `environment_healthy` still -summarizes the full audit. `--target` adds a target check, but without a scope -option the report remains audit-wide. - -When more than one installed engine supports a native target, REA does not pick -one: opening the target fails with `code: "capability_unavailable"`, -`details.selection_reason: "ambiguous"`, and the providers in -`details.candidate_ids`. Choose one once: pass `--provider` on -the CLI or `provider_id` on `open_binary`, or set `REA_ANALYSIS_PROVIDER` as a -standing preference. An explicit selector overrides the environment variable. -The session keeps that choice and never falls back to another engine. -Recovery depends on `details.selection_reason`. For `ambiguous`, choose one of -the candidates. For `provider_unavailable`, the engine you selected needs repair, -so run `rea doctor --provider --json` and follow its remediation. Neither -reason means you have to install every engine. See -[Choosing a deep-analysis provider](#choosing-a-deep-analysis-provider). - -### Install the rea command - -Install the command-line interface: - -```bash -curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash -``` - -The installer adds `rea` to your system and starts setup when run in a terminal. It requires Node.js and npm to be installed already. - -Alternatively, install with npm, then run setup: - -```bash -npm install --global rea-agents -rea setup -``` - -Update either installation with `rea update`. - -### Requirements - -Static JavaScript inspection requires the Node/npm runtime only. Host and -external-tool prerequisites depend on the selected workflow; the native -provider guides describe their supported platforms. - -- macOS 12 or newer -- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux -- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ -- npm; REA does not require or install a particular npm version - -Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghidra](#ghidra-analysis-provider), or [IDA Pro](#ida-pro-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. Ghidra and IDA are bring-your-own providers. - -Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff. - -Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA and macOS `.app`, ZIP, or DMG inventory Evidence can be projected into bundle anatomy, such as XPC services, app extensions, login items, privileged helpers, and launchd plists, with [Apple application analysis](docs/apple-application-analysis.md). - -Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope. - -If something is not working, run: - -```bash -npx -y rea-agents@latest doctor -``` - -`doctor` checks your host, dependencies, analysis tools, and agent configuration without changing them. Use `--json` for structured diagnostics. - -### Linux installation and troubleshooting - -On macOS, setup can install Hopper in `~/Applications` after approval. It verifies the official download and does not need Homebrew or administrator privileges. - -On supported Linux distributions, setup can install Hopper and its demo-session dependencies through your system package manager. You may see a system authorization prompt. Demo sessions use a private virtual display, leaving your desktop alone. See [Hopper installation](docs/installation.md#hopper) for download verification and platform details. - -The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere: - -```bash -export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper -rea doctor --json -``` - -If doctor reports a missing analysis engine even though the file exists, inspect shared-library resolution with: - -```bash -ldd /opt/hopper/bin/Hopper | grep 'not found' -``` - -Install the missing packages and rerun `rea setup`. The Linux demo needs Xvfb, Python 3, X11, and XTEST; approved setup installs these dependencies. If you use the curl installer, add `~/.local/bin` to your shell `PATH` when needed. - -REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence. - -### Ghidra analysis provider - -Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It accepts **Ghidra 12.1.x** and the **64-bit full JDK** that installation declares (`application.java.min` through `application.java.max`). Current 12.1 releases require JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4 and JDK 21. On macOS, your Ghidra installation must also include the native decompiler for your architecture. - -Set the installation paths, then run setup: - -```bash -export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC -export JAVA_HOME=/absolute/path/to/jdk-21 # optional when java and javac resolve from PATH -rea doctor --json -rea setup -rea providers --json -``` - -Setup checks the installations and saves their paths in your selected agents' configuration after approval. Ghidra and Java must already be installed; REA does not download or change them. - -The adapter exposes inventory, function, memory and load-image inspection, plus atomic function annotation edits on Linux and macOS. `annotate_native_function` edits names and entry comments in the session database and returns a refreshed function dossier; executable bytes stay unchanged. Ghidra does not provide GUI controls through REA. - -Opening a Ghidra target selects its provider; the first analysis query starts import and auto-analysis. That query can take longer than a client's default request deadline. See [first-query deadlines and recovery](docs/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery) for an SDK example and cancellation recovery. - -REA analyzes a temporary copy of the target and removes the temporary project when the session closes. Results identify what Ghidra observed and what it could not resolve. Decompilation produces pseudocode rather than the original source. - -Ghidra also imports DOS MZ executables with an explicit 16-bit x86 real-mode profile. Function results include complete observed body ranges, distinguishing owned bytes from the enclosing span. See the [DOS analysis guide](docs/ghidra-dos.md) for addresses, packing, and verification boundaries. -See [optional NativeAOT metadata recovery](docs/ghidra-nativeaot.md) for the pinned -headless adapter, supported layout and existing native-tool workflow. - -Windows Ghidra P0 uses bundled native controls for its read-only native x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification. - -To remove only REA-owned MCP registrations and the managed skill: - -```bash -rea uninstall -rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state -``` - -Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks. - -### IDA Pro analysis provider - -Already have [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) working? REA can reuse its MCP registration for read-only analysis of the current GUI target, or use its database supervisor to open and analyze a supplied binary headlessly. - -```bash -export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json -rea function /absolute/path/to/program main --provider ida --json -``` - -The registration selects `attached` (default, legacy 1.4 tools) or `headless` (modern database supervisor). Setup can preserve this file reference in your selected agent's REA registration. REA adapts existing analysis contracts and manages its own headless database; it leaves an attached GUI database open and never saves it. Results stay live because external IDA database changes are not immutable snapshots. - -See the [IDA provider guide](docs/ida-provider.md) for upstream installation links, exact configuration examples, Windows-native headless operation, lifecycle cleanup, and coverage. The initial real workflows cover a Windows GUI and Windows x64 headless IDA 9.3; other engine/platform combinations remain unverified. Use a package version that includes this adapter; repository main can lead the npm release. - -### CLI or agent? - -| If you want to… | Use | -| ---------------------------------------------------------------- | ------------------------------------------------------------------- | -| Ask an agent to investigate an app and build a feature | Run setup, restart your agent, then describe the task | -| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` | -| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` | -| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` | -| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /path/to/analysis.json` to a deep-analysis command | -| Import source as historical reference | `rea import-reference-source` | -| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` | - -```bash -rea evidence-import /absolute/path/to/evidence/bundle.json -rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json -rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json -``` - -Analyze a JavaScript application directory or ASAR without executing it: - -```bash -rea analyze /absolute/path/to/releases/app.asar --json -rea analyze-javascript-application /absolute/path/to/releases/app.asar --json -``` - -For a directory or `.asar`, generic `rea analyze` automatically selects the -static JavaScript application provider when neither `--provider` nor -`--snapshot` is supplied. Both routes return the analysis and its Evidence -context inline. - -Import an older source tree as a reference. REA keeps it separate from observations of the current app: - -```bash -rea import-reference-source /absolute/path/to/source -``` - -Historical-source import requires safe no-follow file opens and currently runs -on Linux and macOS. Native Windows reports `unsupported_host`; run the import -with Linux REA in WSL or on another supported host. Changing permissions or -reinstalling REA does not enable this Windows workflow. - -Imports read the path supplied to the command. File names do not cause automatic omissions; files are represented by hashes and metadata. To exclude selected paths, set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON string array of ignore patterns. Exports never replace an existing file unless `--overwrite` is explicit. - -Use a snapshot to save successful analysis results and reuse them on later runs. REA reuses a result only when the target bytes, operation, parameters, analysis tool, and settings match. It does not cache changes or cursor-dependent calls. Snapshot files stay local and use owner-only permissions. - -```bash -rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json -# The same exact query can be answered from that snapshot. -rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json -``` - -Exact CLI cached-evidence reads happen before any provider process starts. In MCP sessions, -pass `snapshot_path` to `open_binary` to import a snapshot atomically while -opening its matching target; MCP providers may still start before a cached call -result is returned. Pass `snapshot_path` and, when required, `overwrite: true` to -`close_binary` to save atomically before Hopper resources are released. If the -save fails, REA deliberately leaves the session open. - -## Just ask your agent - -After [setup](#quick-start), restart your agent and ask: +### Ask your agent ```text Understand how search works in the Notes app, show me the evidence, and build a similar feature for my project. ``` -Replace Notes with the app you want to understand, or ask for an overview first. +Replace Notes with your target app and the feature you want to understand. -## The investigation model +### Use the terminal - - - - - - -
-Decompile

-Open an app and recover readable code, strings, names, and other clues about how it works. -
-Understand

-Follow the code from one part of the app to another until the agent can explain how a feature actually works. -
-Recreate

-Turn what the agent learned into a feature for your own product, adapted to your stack, interface, and requirements. -
- -REA shows how it reached its conclusions. It does not claim to recover original source code or automatically clone an application. - -## Why REA - -| | | -| ------------------------ | ----------------------------------------------------------------------------------------------------- | -| **Built for agents** | Ask what an app does and let your agent inspect it instead of guessing. | -| **CLI and MCP** | Run the same reverse-engineering capabilities from your terminal or agent. | -| **Guided setup** | Configure your agent, connect an existing analysis tool, or install Hopper with your approval. | -| **From insight to code** | Understand a feature, then build your own version in the same coding session. | -| **Local by design** | Analysis runs on your supported local host. REA does not upload the app to a hosted analysis service. | -| **Keeps context** | Investigate several apps without starting over for every question. | - -## One prompt, a full investigation - -```text -Reverse engineer the Notes app. Find how offline search works, explain it, -and build a version for my project using TypeScript and SQLite. -``` - -REA gives the agent a clear path from that request to working code: - -| Step | What the agent does | REA tools | -| ---: | --------------------------------------- | ---------------------------------------------------------------- | -| 1 | Opens and identifies the binary | `open_binary`, `binary_overview` | -| 2 | Finds likely offline-search clues | `search_strings`, `search_procedures`, `list_names` | -| 3 | Connects those clues to executable code | `find_xrefs_to_name`, `xrefs`, `procedure_callers` | -| 4 | Reconstructs the relevant control flow | `get_call_graph`, `procedure_callees`, `procedure_info` | -| 5 | Decompiles the relevant routines | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` | -| 6 | Builds the feature in your project | code adapted to your stack, product, and requirements | - -REA handles the app analysis in steps 1 through 5. The agent performs step 6 with its normal file-editing and test tools, using what it learned about the app. - -## Showcase - -### DX-Ball: game reconstruction (in progress) - -[DX-Ball](https://github.com/N0zoM1z0/dx-ball) follows the journey from a classic -Windows game's executable to maintainable C. Using REA's Ghidra provider, the -project traces functions, game state and dependencies, then checks reconstructed -behavior with original-x86 differential tests and pinned-compiler replay. - -A [sound-pan investigation](https://github.com/N0zoM1z0/dx-ball/blob/main/docs/GAMEPLAY_OWNER.md) -turns incomplete pseudocode into a C implementation that passes 3,205 original-x86 -cases and reproduces all 63 compiled function bytes. Follow its -[REA workflow](https://github.com/N0zoM1z0/dx-ball/blob/main/docs/REA.md) -from binary evidence to reconstruction. - -## What agents can do - -- Investigate a feature you like and build a version tailored to your own product. -- Explain how a feature works when its source code is unavailable. -- Reconstruct an app's authentication, storage, update, or networking flow. -- Recover enough structure to document an undocumented format or interface. -- Trace a suspicious behavior from a string or symbol to the code that implements it. -- Turn recovered behavior into product features, tests, migration notes, ports, or interoperable replacements. -- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol. -- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other. - -See [native investigation](docs/native-investigation.md) for keyed archives, instruction/call/type primitives, typed dispatch metadata, value traces and native desktop observation. - -## Tool catalog for investigation - -| Tool family | Count | Examples | -| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets | -| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery | -| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper | -| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction | -| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons | -| Firmware | 2 | Linux firmware region inspection and explicit extraction | -| Android APK | 5 | package and manifest declarations, class search, member inventories, method decompilation, and incoming static references | -| Browser observation | 11 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons | -| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation | -| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events | -| Application workflows | 13 | captured website script export; Android/Apple inventory projections; cross-layer feature traces, build comparisons, historical source mapping, static return-shape comparison, and reconstruction checks | -| Workspace and observation | 21 | sessions, evidence bundles, navigation context, process/artifact/function comparisons, and open-question tracking | - -The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness records direct behavioral captures. - -## Current status - -REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites. `rea capabilities` and `rea providers` describe the binary-session providers and auxiliary operations; they are not an inventory of every browser, Android, or application workflow. Use the connected MCP tool list and `binary_session` tool availability for the full MCP surface, and the relevant guide for each tool's prerequisites. Repository main can be ahead of [the npm release](docs/installation.md#released-package-and-main). - -Static Android APK inspection supports Linux and macOS; the current metadata bridge is verified on macOS arm64 with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage. - -- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through a selected deep provider. Hopper and Ghidra cover broad inventory and function analysis; the IDA adapter supplies its documented read-only function/string operations. Hopper also accepts `.hop` databases and supports annotations. -- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support. -- **JavaScript and Electron:** Map modules, imports, source maps, routes, IPC channels, storage, and native add-ons without running the app. Compare builds and trace a feature across the recovered graph. Dynamic and ambiguous relationships remain unresolved. See [JavaScript application workflows](docs/javascript-application-workflows.md). -- **Websites:** Inspect a selected page in an existing Chrome-family browser. Capture page structure, network metadata, script evidence, and screenshots requested by the call. Passive observation does not navigate or execute page JavaScript. See [browser observation](docs/browser-observation.md). - - Export retained scripts with `export_web_scripts` / `rea export-web-scripts` - into a verified local directory, then use the existing JavaScript analysis - and tracing tools. Source URLs, frame or transaction references, competing - versions, and missing-source states remain inline. See - [captured website scripts](docs/website-script-export.md). - Inspect a selected node's listener sources or observe an externally triggered - execution window with `inspect_web_event_listeners` / `observe_web_execution`. - Precise coverage resets counters and affects optimized execution; see - [website runtime attribution](docs/web-runtime.md). - Trace one exported script's native imports with `trace_web_module_imports` / - `rea trace-web-module-imports`, preserving query/fragment identity and optional - import-map context. Requires caller-supplied Chromium via - `REA_BROWSER_EXECUTABLE`; see [module relationships](docs/website-module-trace.md). - -- **Source locations:** Trace one retained website script point through a selected - local source map with `rea trace-web-source-location`. See - [captured website source locations](docs/web-source-location.md) for byte - identities, embedded original content and coverage limits. - Recover readable modules from selected local bundles with - `recover_javascript_sources` / `rea recover-javascript-sources`, then pass - the returned `analysis_input` to static analysis. This optional Linux x64 - adapter requires caller-supplied Wakaru 1.13.0; see - [JavaScript source recovery](docs/javascript-recovery.md). - -- **Electron and Node runtime observation:** Inspect selected Electron pages or attach to a Node/Electron V8 Inspector target. Inspector observation records script locations and execution-context events; it does not infer imports, IPC activity, or which modules executed. See [runtime observation](docs/javascript-runtime-observation.md). -- **.NET assemblies:** Inspect metadata and CIL instructions, compare builds, and check declared native dependencies without loading or running the assembly. Imported decompiler output is labeled as analyst inference. See [managed-code analysis](docs/managed-code-analysis.md). -- **Controlled behavior capture:** Run process, browser, or Electron scenarios with the target, actions, and lifecycle declared in each request, then compare the resulting evidence. Missing observations cannot establish that two runs behaved the same way. -- **Evidence and comparison:** Save results with artifact identity, provider, locations, confidence, and limitations. Export or import bundles, compare artifacts and functions, and connect static findings to runtime observations without claiming causality from correlation. -- **Open questions:** Track unresolved findings, contradictions, and follow-up probes. Reconstruction checks report pass, fail, or unknown rather than treating missing evidence as a pass. -- **Guided workflows:** Start six [MCP investigation workflows](docs/mcp-prompts.md) with suggestions based on your current session. - -Windows x64 Ghidra P0 supports native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS with 25 read-only operations. Linux/macOS Ghidra additionally supports atomic session-scoped function names and entry comments. Ghidra has no GUI controls; Windows P0 has no mutation authority. - -### Website observation with CDP - -REA can inspect an already-running Chrome-family browser through a literal loopback CDP endpoint. Each request names the endpoint and target; an optional origin filter can narrow discovery: +Inspect an extracted JavaScript/Electron app directory or ASAR: ```bash -rea list-browser-targets http://127.0.0.1:9222 --json -rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --json +npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json ``` -The eight passive browser tools work through both CLI and MCP. They inspect the selected page without navigating, clicking, or evaluating its JavaScript. Credentials, cookies, authorization headers, and raw payload values are not retained. A request selects whether to include script sources, accessibility text, screenshots, or console and payload summaries. REA cannot observe activity that happened before it attached. See [browser observation](docs/browser-observation.md) for browser startup, capture options, and limits. +The result includes modules, imports, Electron boundaries and their evidence. +Replace the path with your target, such as `"D:/apps/example"` on Windows. -### Controlled browser scenarios - -`capture_browser_scenario` runs a caller-declared sequence of browser actions through -Playwright. Unlike passive observation, it can interact with the page. Each -step records evidence such as screenshots, page structure, navigation, and -network activity. Missing or truncated observations cannot establish that two -runs behaved the same way. - -```bash -rea capture-browser-scenario ./scenario.json --json -``` - -Launch mode owns a temporary browser profile and removes it after terminating -the launched browser. Connect mode accepts one exact loopback CDP target and -disconnects without closing the external browser. The request supplies the -selected executable or endpoint, actions, and any origin or environment -selections needed by the scenario. Scenario JSON contains secret references and -environment-variable names, never secret values. The default capture retains -only the final URL; request `dom`, `accessibility`, or `screenshot` when an -interaction changes the page without navigating. For asynchronous updates, -wait for a page-specific result-ready condition before capture. See the -[browser scenario contract](docs/browser-scenario-contract.md), including its -interaction example. - -### Node and Electron V8 Inspector observation - -Node and Electron runtime observation attaches to an existing Inspector target named in the request: - -```bash -rea list-javascript-runtime-targets http://127.0.0.1:9229 --json -rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \ - --runtime-kind node --json -``` - -REA records script locations and execution-context events without evaluating -code or setting breakpoints. These observations do not establish import -relationships, event activity, IPC, process identity, or Electron roles. See -[Node and Electron runtime observation](docs/javascript-runtime-observation.md) -for the exact coverage. - -Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, and generated schemas. - -## Roadmap - -The [current status](#current-status) section describes shipped capabilities. These are the next areas of work. - -### Now - -1. **Keep documentation accurate:** update the generated catalog and documentation checks when tools, providers, setup options, or versions change. -2. **Test more native binaries:** expand architecture and indirect-call coverage across Hopper and Ghidra. - -### Next - -1. **Connect more application layers:** add static extractors and runtime observations to feature traces. -2. **Extend .NET analysis:** improve comparisons of obfuscated assemblies and connect managed findings to verified native analysis. See the [managed-code guide](docs/managed-code-analysis.md). -3. **Compare more runtime behavior:** expand process, protocol, filesystem, reconnect, and version-comparison coverage. - -### Later - -1. **Expand browser and Electron interaction:** add scenario actions beyond the current click and wait operations. -2. **Observe native apps at runtime:** explore LLDB, Frida, system logs, and native API tracing. -3. **Evaluate more tools and targets:** assess Binary Ninja, Rizin, LIEF, Windows-native tools, mobile apps, and firmware. IDA support already ships via the bring-your-own upstream adapter; see the [IDA provider guide](docs/ida-provider.md). - -Setup already lets you choose agent integration and Hopper installation. Support for installing additional analysis tools is future work, described in the [installation roadmap](docs/roadmap.md). - -See [provider evaluation](docs/provider-evaluation.md) for coverage and remaining requirements, and the [native investigation guide](docs/native-investigation.md) for UI, dispatch, and value-flow analysis. - -## Using REA with other agents - -Setup offers supported agent integrations for selection. Existing REA registrations are selected by default; newly detected agents remain unselected until chosen. Any agent that supports local MCP servers can use the configuration below. - -### Manual MCP configuration - - - -```json -{ - "mcpServers": { - "rea": { - "command": "npx", - "args": ["-y", "rea-agents@4.1.0", "mcp"] - } - } -} -``` - - - -Persistent registrations should use one exact package version. `rea setup` -maintains that pin, updates the bundled skill at the same time, and gives Codex -a 30-second startup allowance for a cold package-runner start. `rea update` -installs an exact release and verifies the new executable. It returns an -unapplied maintenance plan for existing REA integrations, with a scoped setup -command to review and approve their changes. Restart affected agents afterward. - -MCP clients that support prompts can also discover six ordered investigation -workflows through `prompts/list`. Their optional identifier arguments use the -current session for bounded `completion/complete` suggestions; see -[Guided MCP prompts and completion](docs/mcp-prompts.md). - -## How it works - -```mermaid -flowchart LR - Agent["Agent"] --> REA["REA
CLI + MCP"] - Terminal --> REA - REA --> Session["Target-bound session router"] - Session --> Registry["Deep-provider registry
deterministic selection"] - Registry --> Hopper["Hopper provider"] - Registry --> Ghidra["Ghidra provider
inventory + function analysis + annotations"] - Registry --> Ida["IDA MCP provider
attached GUI or owned headless database"] - Hopper --> Runtime["Owned provider runtime
deadline + bounded diagnostics + cleanup"] - Ghidra --> Runtime - Ida --> IdaRuntime["Upstream MCP lifecycle
live observations + owned database cleanup"] - IdaRuntime --> Target - Session --> Native["Native macOS provider"] - Session --> Artifact["Artifact graph provider"] - REA --> Browser["Browser CDP provider"] - REA --> Android["Android static provider
headless JADX adapter"] - Android --> Runtime - REA --> Firmware["Firmware providers
Binwalk / Unblob adapters"] - Firmware --> Runtime - REA --> Process["Process capture provider"] - Runtime --> Target["Target software"] - Process --> Target - Native --> Target - Artifact --> Target -``` - -The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger for the session. - -## CLI - -The agent workflow above is the easiest way to use REA. For a one-off overview from the Terminal: - -```bash -npx -y rea-agents@latest analyze /Applications/Notes.app -npx -y rea-agents@latest inspect /Applications/Notes.app -npx -y rea-agents@latest search /Applications/Notes.app "offline" -npx -y rea-agents@latest function /Applications/Notes.app 0x1000 -npx -y rea-agents@latest xrefs /Applications/Notes.app 0x1000 -npx -y rea-agents@latest trace /Applications/Notes.app "offline" -npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json -npx -y rea-agents@latest capabilities -npx -y rea-agents@latest providers -``` - -Run `npx -y rea-agents@latest --help` for direct decompilation, bounded search and -other options. `analyze` and `inspect` share the same overview workflow; -`function`, `xrefs`, and `trace` return the same Evidence envelopes as MCP. - -Or install the `rea` command globally: +To install the `rea` command for regular use: ```bash npm install --global rea-agents rea --help +``` + +For native analysis, configure a provider first. See the +[CLI and Evidence guide](docs/cli.md) for native commands, provider selection, +snapshots and scripting. + +### Update REA + +REA changes quickly, and new releases include frequent bug fixes. Keep your +installation up to date. + +For an npm-installed CLI: + +```bash rea update -rea mcp ``` -REA accepts a Mac `.app` folder directly. If an agent cannot find an app by name, tell it where the app is installed. +To refresh your agent registrations and skill, run the setup command printed +by the update. -### Choosing a deep-analysis provider - -Choose which analysis tool to use from the CLI: +If you use `npx`, update your agent setup with: ```bash -rea providers --json -rea analyze /absolute/path/to/program --provider hopper -REA_ANALYSIS_PROVIDER=hopper rea decompile /absolute/path/to/program 0x1000 +npx rea-agents@latest setup ``` -For MCP, pass the optional selector on `open_binary`: +Review the setup changes and restart your agent. For one-off CLI commands, +use `npx rea-agents@latest` followed by the command. -```json -{ - "path": "/absolute/path/to/program", - "provider_id": "hopper" -} -``` +## How REA works -Use `--provider`, or `provider_id` in MCP, to choose Hopper, Ghidra, or IDA for a target. This choice overrides `REA_ANALYSIS_PROVIDER`. +Your agent calls REA through MCP to inspect the target and trace relevant code. +REA returns findings with their evidence. The agent uses them to ask follow-up +questions, explain the behavior, or write and test an implementation. +CLI commands use the same workflows. -With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool. +![REA investigation flow: your agent asks about a local target, REA inspects and traces it using analysis tools, and the agent uses the returned code, references and unknowns to explain, implement and test.](website/public/assets/figures/rea-investigation-flow.svg) -Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls. +[Open the full-size figure](website/public/assets/figures/rea-investigation-flow.svg). -The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details. + -### CLI exit status +## What you can analyze -| Status | Meaning | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `0` | The operation completed. Results may still include warnings, partial evidence, or unresolved questions. | -| `1` | The operation could not complete, for example because of invalid input, host permission denial, cancellation, or timeout. Structured output reports the reason when available. | -| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. | +REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+, plus npm. +Additional tools and host support depend on the target: -`setup --dry-run` returns status `planned` and exits `0`. A setup result with -status `cancelled` also exits `0`. -`setup` returns `1` for `needs_confirmation` or `needs_human` -because configuration is not ready; rerun it after approval or remediation. -`doctor` returns `1` when required checks for its readiness scope are unhealthy. -Unavailable optional provider prerequisites remain visible as informational -diagnostics and do not block setup or unrelated providers. Output format, full envelopes, -filters, and token controls never change the operation status. +| Target | What REA returns | Requirements and guide | +| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://morluto.github.io/rea/guides/native/) | +| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) | +| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier; [offline EVM guide](docs/evm-bytecode.md) | +| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) | +| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](https://morluto.github.io/rea/guides/javascript/) | +| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://morluto.github.io/rea/guides/browser/) | +| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) | +| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | Static inspection; [managed-code guide](docs/managed-code-analysis.md) | +| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS; [Android guide](docs/android-analysis.md) | +| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) | +| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) | +| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) | -When REA feeds a shell pipeline, enable `pipefail` so a downstream formatter -cannot hide its failure: +Static JavaScript and .NET inspection read the supplied files without running +the application. Runtime capture runs or interacts with the selected target +using your user permissions; each runtime guide describes its effects. -```bash -set -o pipefail -rea inspect-artifact ./app.asar --json | jq . > inspection.json -``` + -## Current Hopper provider +Native formats and host support vary by provider. See +[Hopper and Ghidra setup](docs/installation.md#hopper), the +[IDA guide](docs/ida-provider.md), and +[experimental Windows Ghidra support](docs/windows-ghidra-p0.md). +Ghidra also supports [16-bit DOS analysis](docs/ghidra-dos.md). +For provider selection, see the [CLI guide](docs/cli.md#choose-a-provider). +Check [release availability](docs/installation.md#released-package-and-main) +for features added since the latest npm release. -REA starts Hopper when an operation needs it. On macOS, Hopper may bring its window or a dialog to the foreground even though REA requests background startup. Demo or license prompts may need your attention. +## Showcases -Hopper handles one analysis request at a time. Cancelling your wait does not stop work already running inside Hopper; the session reports whether it is still busy. Successful decompilation results are cached until a relevant rename or comment change. +### DX-Ball: reconstruct a sound-pan calculation -Use `rea instructions` when you only need assembly instructions for a function. It avoids decompilation and a whole-program inventory. +Follow a sound call into its position-to-pan helper, inspect the instructions, +and turn incomplete pseudocode into C. The reconstruction passes 3,205 +original-x86 cases and reproduces all 63 compiled function bytes. -Closing a session shuts down REA's bridge and removes its temporary socket directory while preserving a Hopper application you may be using. If cleanup cannot be verified, `close_binary` reports `cleanup_incomplete` and the affected resources. +[Read the case study](https://morluto.github.io/rea/showcase/dx-ball/) · +[Reconstruction repository](https://github.com/N0zoM1z0/dx-ball) -## Process capture +### Notion: trace the Electron clipboard bridge -Process capture runs the exact executable and scenario declared in the request, -with the requested working directory and environment. Filesystem observation -paths select what to snapshot. The process runs with your user permissions; -Process Capture records behavior and is not a security sandbox. +Find the renderer's clipboard API, follow it through preload and IPC into the +main process, and inspect the rich clipboard format. -Capture a scenario or compare two saved Process Capture Evidence records: +[Read the case study](https://morluto.github.io/rea/showcase/notion/) -```bash -rea capture-process ./authority-scenario.json --json > authority-capture.json -rea capture-process ./reconstruction-scenario.json --json > reconstruction-capture.json -rea compare-process-captures authority-capture.json reconstruction-capture.json --json -``` +### TH04: recover a DOS bullet-ring calculation -The comparison reports each observed dimension separately and identifies the -first terminal, interaction, exit, filesystem, or process divergence. -See [Process Capture](docs/process-capture.md) for scenario fields, limits, and -evidence boundaries. +Inspect the original PC-98 game's 16-bit instructions, recover the fixed and +aimed angle calculations, and compare the reconstructed C++ with the +historical compiler output. -Process Capture currently runs on Linux and macOS with a working native PTY -backend. Native Windows capture remains unavailable until the PTY adapter can -verify descendant cleanup; installing or reinstalling its Windows PTY binary -does not enable capture. For Linux commands, run Linux REA inside WSL. -Comparing existing capture Evidence remains available on Windows. +[Read the case study](https://morluto.github.io/rea/showcase/th04/) · +[Reconstruction repository](https://github.com/N0zoM1z0/th04) -On supported capture hosts, a missing or incompatible PTY binary can be fixed -by reinstalling REA for the current platform, architecture, and Node.js version -with optional dependencies enabled. Use separate scenario and output files; -`--json` produces the JSON consumed by the comparison command. - -ASAR inventory verifies Electron integrity metadata for both archive entries -and `.asar.unpacked` companion files. Integrity failures identify the logical -path, declared and calculated SHA-256 values, and whether the entry was -unpacked; REA does not silently accept the mismatched artifact. If a supplied -ASAR declares unpacked companion bytes that are absent from the local artifact -set, REA keeps that occurrence as `unavailable` and continues analyzing the -embedded JavaScript instead of treating the missing native/resource bytes as -verified or absent. - -## Security model - -Analysis runs locally. REA communicates with Hopper and Ghidra through authenticated private local sockets and with IDA through its configured local MCP registration. Your agent or model provider has its own data policy. - -Runtime requests act on the declared target and lifecycle. Analysis tools and launched targets run with your user permissions, and native UI capture still depends on macOS Accessibility and Screen Recording access. Static JavaScript analysis does not execute extracted modules; use direct browser, Electron, or process capture when runtime behavior is needed. - -Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md). +If you've used REA on something interesting, we'd love to see it. Share your +case in an [issue](https://github.com/morluto/rea/issues) or a +[pull request](https://github.com/morluto/rea/pulls), including the target, +your question, how REA helped, and what you found. ## FAQ -
-Does Hopper need to be running before I start REA? - -No. REA starts Hopper when an operation needs it. An already-running Hopper application is also supported. - -
- -
-Why did Hopper appear in front of my other windows? - -Hopper's launcher internally activates the application. REA requests background startup, but macOS and Hopper may still bring a window or dialog forward. See [Current Hopper provider](#current-hopper-provider). - -
- -
-Does REA include Hopper? - -No. Setup can install Hopper for you, but Hopper remains separate software with its own license. REA supplies the CLI, MCP server, and workflows that make it usable by agents. - -
- -
-Does REA install or include Ghidra or Java? - -No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-analysis-provider). - -
- -
-Does REA upload the app? - -REA has no hosted analysis service. Current providers analyze artifacts and capture behavior locally. Your agent or model provider may have its own data policy, so review that separately. - -
- -
-Can REA recover the original source code? - -No decompiler can guarantee the original source. REA gives an agent pseudocode, assembly, symbols, strings, metadata, and relationships that it can use to explain or compatibly recreate observed behavior. - -
-
Which agents can use REA? -Any agent that can run a local MCP server can use the manual configuration. Setup offers the supported integrations listed in [Installation and setup](docs/installation.md#supported-agents); existing REA registrations are selected by default, and other detected agents require selection. +Any agent that supports local MCP servers. Setup configures the +[supported agents](docs/installation.md#supported-agents); other clients can use +[manual MCP registration](docs/installation.md#mcp-registry).
-## Development +
+Do I need Hopper, Ghidra or IDA? -See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution checks, and [docs/testing.md](docs/testing.md) for test scopes and real-tool verification. `npm run docs:check` checks committed generated documents; `npm run docs:generate` regenerates them. +Deep native analysis uses one of them. Static JavaScript and .NET inspection +work without a native analysis engine. Setup can install Hopper after approval; +Ghidra and IDA use your existing installations. See [provider setup](docs/installation.md#hopper). -`npm run verify:agent` evaluates native, JavaScript, managed, and browser investigation tasks through a real local Codex CLI. Its report covers tool selection, repeated calls, token use, completion quality, and handling of permissions and unknowns. +
-`npm run evidence:generate` regenerates the managed conformance manifest and Evidence completion ledger from live verification results. `npm run evidence:check` reruns verification and checks for drift. Unsupported claims remain explicit and do not count as passes. See [testing](docs/testing.md) for verification commands and process-cleanup checks. +
+Do I need to start Hopper first? + +REA starts Hopper when an operation needs it. On macOS, a first-run dialog may +ask you to choose demo mode or activate your license. See +[Hopper startup and troubleshooting](docs/installation.md#launcher-paths-and-troubleshooting). + +
+ +
+What does installing the skill from skills.sh do? + +The skill supplies investigation instructions for your agent. Use `rea setup` +to register REA's MCP server and install the matching instructions, then restart +your agent. See [skill-only installation](docs/installation.md#skill-only-installation). + +
+ +
+What code does REA return? + +Native analysis returns pseudocode and assembly. JavaScript/Electron analysis +recovers modules and their relationships. Your agent uses these findings to +write and test an implementation; the [showcases](#showcases) give worked examples. + +
+ +
+Does REA upload my app? + +REA analyzes targets locally. Your agent receives the tool results, and its +model provider has its own data policy. + +
+ +
+What should I do if I hit a bug? + +Update first; a recent release may already fix it. + +For an npm-installed CLI: + +```bash +rea update +``` + +For agent setup through `npx`: + +```bash +npx rea-agents@latest setup +``` + +If you're using an agent, complete the [setup refresh](#update-rea) and restart +it. Retry the same task. If the problem persists, [open an issue](https://github.com/morluto/rea/issues) +with your REA version, target type, steps to reproduce and error output. + +
+ +## Documentation + +Start with the website's [worked guides](https://morluto.github.io/rea/guides/). +For exact options, prerequisites and result contracts: + +- [Installation and setup](docs/installation.md): agent registration, provider configuration, updates and uninstall. +- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine. +- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses. +- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations. +- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands. +- [Roadmap](docs/roadmap.md): planned work and capability trackers. + +Report vulnerabilities through [SECURITY.md](SECURITY.md). + +## Contributing + +We'd love your help with REA! [Open an issue](https://github.com/morluto/rea/issues) to +report a bug or suggest a feature, or [send a pull request](https://github.com/morluto/rea/pulls) +to improve the code or docs. + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks, +[testing](docs/testing.md) for verification lanes, and the +[architecture map](docs/architecture.mermaid) for the project structure. ## Project links -[npm](https://www.npmjs.com/package/rea-agents) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Hopper](https://www.hopperapp.com/) · [Ghidra](https://github.com/NationalSecurityAgency/ghidra) +[Website](https://morluto.github.io/rea/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md) ## License diff --git a/README_ar.md b/README_ar.md index 5558a91660..0e8e70fc87 100644 --- a/README_ar.md +++ b/README_ar.md @@ -15,6 +15,10 @@ [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) [![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM) +🎉 **وصلنا إلى 20,000 نجمة على GitHub، شكرًا لكم!** + +شكرًا لكل من يستخدم REA ويبلّغ عن الأخطاء ويختبر الإصدارات ويساهم في إصلاحها. + morluto%2Frea | Trendshift **[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)** @@ -138,7 +142,7 @@ rea setup ### المتطلبات - macOS 12 أو أحدث -- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت +- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت أو CachyOS - Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا. @@ -153,7 +157,7 @@ rea setup يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم. -مسار Hopper الافتراضي على Linux هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'` للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper). +على Linux، يفضّل REA الملف التنفيذي `/opt/hopper/bin/Hopper`، وإذا لم يكن متاحًا يتحقق تلقائيًا من `~/.local/share/rea/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /absolute/path/to/Hopper | grep 'not found'` على مسار Hopper الفعلي للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper). ### التحديث والإزالة @@ -213,20 +217,20 @@ rea setup ## منصة أدوات التحقيق -| عائلة الأدوات | العدد | الاستخدام | -| -------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| الفحص الأصلي | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والاستدعاءات والمراجع والتعليقات وقراءة البايتات ومواضع الملفات | -| سير عمل التحقيق | 14 | نظرات عامة على التطبيقات وملفات الدوال وواجهات API الأصلية والتوزيع وفك الترجمة الدفعي وتتبع الميزات ومسارات الاستدعاء ومخططاته واكتشاف Swift وObjective-C | -| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift دون تشغيل Hopper | -| مخطط المخرجات | 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 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء | -| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة | +| عائلة الأدوات | العدد | الاستخدام | +| ---------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| الفحص الأصلي | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والاستدعاءات والمراجع والتعليقات وقراءة البايتات ومواضع الملفات | +| سير عمل التحقيق | 14 | نظرات عامة على التطبيقات وملفات الدوال وواجهات API الأصلية والتوزيع وفك الترجمة الدفعي وتتبع الميزات ومسارات الاستدعاء ومخططاته واكتشاف Swift وObjective-C | +| أدوات الملفات الثنائية | 10 | بيانات Mach-O والتوقيعات وplist والمعماريات وأسماء Swift ومراقبة الاستدعاءات عبر LLDB على macOS؛ تخطيط ELF وأدلة الحماية الثابتة وسجلات الأعطال على Linux | +| مخطط المخرجات | 7 | جرد الدلائل والحزم وملفات Interface Builder المترجمة وكتالوجات موارد Apple وتحليل تحميل Mach-O dylib والاستخراج واستدلال واجهات EVM دون اتصال | +| PE/CLI المُدار | 7 | هوية تجميعات .NET وبياناتها الوصفية وتعليمات CIL والاعتماديات الأصلية واستيراد إعادة البناء ومقارنة الإصدارات | +| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة على Linux واستخراجها الصريح | +| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف وقوائم الأعضاء وفك ترجمة الأساليب والمراجع الثابتة | +| مراقبة المتصفح | 12 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط | +| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل | +| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ | +| سير عمل التطبيقات | 13 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء | +| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة | ## خطة العمل @@ -243,7 +247,7 @@ rea setup "mcpServers": { "rea": { "command": "npx", - "args": ["-y", "rea-agents@4.1.0", "mcp"] + "args": ["-y", "rea-agents@6.0.0", "mcp"] } } } @@ -313,7 +317,7 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
هل يعمل REA على Linux أو Windows؟ -يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق. +يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت وCachyOS. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
diff --git a/README_ja.md b/README_ja.md index 650920fd77..f89f9d4432 100644 --- a/README_ja.md +++ b/README_ja.md @@ -4,9 +4,9 @@ # REA:あらゆるものをリバースエンジニアリング -### エージェントでアプリの動作からネイティブバイナリまで調べる。 +### バイナリ、アプリ、実行時の動作をひとつの MCP でリバースエンジニアリング。 -**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。** +**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。** [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) @@ -15,6 +15,10 @@ [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) [![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM) +🎉 **GitHub スターが 20,000 を突破しました。ありがとうございます!** + +REA を使い、問題を報告し、ビルドをテストし、修正を届けてくださる皆さんに感謝します。 + morluto%2Frea | Trendshift **[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)** @@ -45,11 +49,11 @@ --- -アプリの機能を自分のプロダクトにも取り入れたいときは、REA を使ってエージェントに調査を依頼できます。ソースコードがなくても、アプリを調べ、仕組みと根拠を示し、あなたのプロジェクト向けに同様の機能を実装できます。 +自分のプロダクトにも欲しい機能を他のアプリで見つけたら、REA を使ってエージェントに調査を依頼できます。エージェントはソースコードがなくてもアプリを調べ、機能の仕組みを説明して根拠を示し、あなたのプロジェクト向けの実装まで進められます。 -REA はネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトの解析ツールを提供します。エージェントからもターミナルからも使えます。解析はローカルで実行され、結果には根拠と制限が含まれます。 +REA は、ネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトを調べるためのツールをエージェントに提供します。同じツールはターミナルからも使えます。解析はローカルで実行され、結果には各結論の根拠と制限が含まれます。 -Setup はエージェントを設定し、既存の Hopper または Ghidra に接続します。承認後に Hopper をインストールすることもできます。 +セットアップは REA をエージェントに登録し、対応するワークフロー手順もインストールします。ネイティブ解析には既存の Hopper または Ghidra を使えます。承認すれば、セットアップで Hopper をインストールすることもできます。静的な JavaScript 解析にはどちらのエンジンも不要です。 ## エージェントに頼むだけ @@ -59,7 +63,7 @@ Setup はエージェントを設定し、既存の Hopper または Ghidra に メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。 ``` -メモアプリを調べたいアプリに置き換えるか、まず概要を依頼してください。 +「メモアプリ」を調べたいアプリに置き換えてください。まずは概要の調査を依頼することもできます。 ## バイナリから動作へ @@ -67,56 +71,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 +147,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 +158,56 @@ rea setup ### 要件 +静的な JavaScript 解析に必要なのは Node.js と npm だけです。ホストと外部ツールの前提条件は使うワークフローによって異なり、ネイティブ解析プロバイダーの対応プラットフォームは各ガイドで説明しています。 + - macOS 12 以降 -- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux -- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm +- Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux、または CachyOS +- 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` です。別の場所には `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、`ldd /opt/hopper/bin/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 +220,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 の検出 | +| ネイティブバイナリ | 10 | macOS の Mach-O メタデータ、署名、plist、アーキテクチャ、Swift シンボル、LLDB による呼び出し観測;Linux の ELF レイアウト、静的な保護機構の証拠と記録済みクラッシュ | +| 成果物グラフ | 7 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、Mach-O dylib のロード解決、抽出、オフライン EVM インターフェース推定 | +| マネージド PE/CLI | 7 | .NET の識別情報、メタデータ、CIL 命令、ネイティブ依存関係、再構築結果のインポート、ビルド比較 | +| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 | +| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッドの逆コンパイル、静的な被参照 | +| ブラウザー観察 | 12 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、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 サーバーに対応するエージェントであれば、次の設定でも接続できます。 @@ -232,7 +276,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf "mcpServers": { "rea": { "command": "npx", - "args": ["-y", "rea-agents@4.1.0", "mcp"] + "args": ["-y", "rea-agents@6.0.0", "mcp"] } } } @@ -240,29 +284,33 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf +常用する登録では、パッケージのバージョンを 1 つに固定してください。`rea setup` はその固定バージョンを維持し、同梱のスキルも同時に更新します。 + ## 仕組み ```mermaid flowchart LR Agent["コーディングエージェント"] --> REA["REA
CLI + MCP"] Terminal["ターミナル"] --> REA - REA --> Hopper["Hopper 解析操作"] + REA --> Hopper["Hopper プロバイダー"] + REA --> Ghidra["Ghidra プロバイダー
インベントリ + 関数解析 + 注釈"] + REA --> Ida["IDA MCP プロバイダー
attached GUI または headless データベース"] Hopper --> App["あなたのアプリ"] - REA --> Ghidra["Ghidra 読み取り専用解析
インベントリ + 関数解析"] 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 +320,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 -
Hopper を先に起動する必要がありますか? +
Hopper を先に起動しておく必要はありますか? -いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。 +いいえ。REA は必要になった時点で Hopper を起動します。すでに起動している Hopper でも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
-
REA に Hopper は含まれますか? +
REA に Hopper は含まれていますか? -含まれません。Setup で Hopper をインストールできますが、Hopper は独自のライセンスを持つ別製品です。REA は CLI、MCP サーバー、エージェント向けワークフローを提供します。 +含まれていません。セットアップで Hopper をインストールできますが、Hopper は独自のライセンスを持つ別製品です。REA は、エージェントが Hopper を使えるようにする CLI、MCP サーバー、ワークフローを提供します。
-
バイナリはアップロードされますか? +
REA は Ghidra や Java をインストールしたり同梱したりしますか? -REA にホスト型解析サービスはありません。ローカル Unix ソケット経由で Hopper を操作します。エージェントやモデル提供者のデータポリシーは別途確認してください。 +いいえ。REA は既存の Ghidra のインストールに接続します。[要件](#要件)にある Ghidra と Java のパスを設定してから、セットアップを実行してください。 + +
+ +
アプリはアップロードされますか? + +REA にホスト型の解析サービスはありません。現在のプロバイダーは、成果物の解析と動作のキャプチャをローカルで行います。エージェントやモデルの提供元には独自のデータポリシーがあるため、別途確認してください。
元のソースコードを復元できますか? -保証できません。REA は疑似コード、アセンブリ、シンボル、文字列、メタデータ、関係を提供し、エージェントが観察した動作を説明または互換再現できるようにします。 +元のソースコードを確実に復元できるデコンパイラーはありません。REA はエージェントに疑似コード、アセンブリ、シンボル、文字列、メタデータ、それらの関係を提供し、観察された動作の説明や互換性のある再現に役立てられるようにします。
diff --git a/README_ko.md b/README_ko.md index 7d82a03623..7aa2b334ef 100644 --- a/README_ko.md +++ b/README_ko.md @@ -15,6 +15,10 @@ [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) [![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM) +🎉 **GitHub 스타 20,000개, 감사합니다!** + +REA를 사용하고, 버그를 제보하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다. + morluto%2Frea | Trendshift **[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)** @@ -109,7 +113,7 @@ AI 코딩 어시스턴트에 스킬을 추가하면 더 풍부한 컨텍스트 npx skills add morluto/rea --skill reverse-engineer-anything ``` -이 스킬은 REA의 조사 워크플로를 제공합니다. 위 setup을 실행해 REA를 에이전트에 연결하고 분석 도구를 설정하세요. Setup은 기본적으로 버전에 맞는 스킬을 설치합니다. 이 명령은 저장소 버전의 스킬을 설치합니다. +이 스킬은 REA의 조사 워크플로를 제공합니다. 위 Setup을 실행해 REA를 에이전트에 연결하고 분석 도구를 설정하세요. Setup은 기본적으로 버전에 맞는 스킬을 설치합니다. 이 명령은 저장소 버전의 스킬을 설치합니다. 압축을 해제한 JavaScript/Electron 앱 트리나 ASAR라면 MCP 설정이나 네이티브 엔진 없이 바로 분석할 수 있습니다. @@ -117,7 +121,7 @@ npx skills add morluto/rea --skill reverse-engineer-anything npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json ``` -경로를 조사 대상으로 바꾸세요(Windows에서는 `"D:/apps/example"` 같은 경로). 이 정적 워크플로에서는 이런 경로도 지원됩니다. 네이티브 분석에는 전용 엔진 설정이 필요합니다. +경로를 조사 대상의 경로로 바꾸세요(Windows에서는 `"D:/apps/example"` 같은 경로). 이 정적 워크플로에서는 이런 경로도 지원됩니다. 네이티브 분석에는 전용 엔진 설정이 필요합니다. ### rea 명령 설치하기 @@ -139,12 +143,12 @@ rea setup ### 요구 사항 - macOS 12 이상 -- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux +- Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux 또는 CachyOS - Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm 네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다. -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를 설치하거나 업데이트하지 않습니다. @@ -154,7 +158,7 @@ Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Ja `npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요. -Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 `ldd /opt/hopper/bin/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)를 참고하세요. ### 업데이트와 제거 @@ -164,7 +168,7 @@ Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다 ## 현재 지원 범위 -저장소의 현재 기능과 플랫폼 요구 사항은 [영문 지원 가이드](README.md#current-status)에 설명되어 있습니다. main은 [npm 릴리스](docs/installation.md#released-package-and-main)보다 앞설 수 있습니다. +저장소의 현재 기능과 플랫폼 요구 사항은 [영문 지원 가이드](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)을 참고하세요. @@ -187,7 +191,7 @@ TypeScript와 SQLite를 사용해 제 프로젝트에 맞는 버전을 구현해 | 5 | 관련 루틴 디컴파일 | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` | | 6 | 프로젝트에 기능 구현 | 기술 스택, 제품, 요구 사항에 맞는 코드 | -REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이전트가 일반 파일 편집 및 테스트 도구로 수행합니다. +REA는 1~5단계의 바이너리 분석을 처리합니다. 6단계는 에이전트가 일반 파일 편집 및 테스트 도구로 수행합니다. ## 에이전트가 할 수 있는 일 @@ -203,20 +207,20 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이 ## 조사 도구 카탈로그 -| 도구 분류 | 수 | 용도 | -| ----------------- | --: | ------------------------------------------------------------------------------------------------------------------------- | -| 네이티브 검사 | 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 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 | -| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 | +| 도구 분류 | 수 | 용도 | +| ----------------- | --: | ---------------------------------------------------------------------------------------------------------------------------------------- | +| 네이티브 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조, 주석, 바이트 읽기, 파일 오프셋 | +| 조사 워크플로 | 14 | 앱 개요, 함수 기록서, 네이티브 API와 디스패치, 일괄 디컴파일, 기능 추적, 호출 경로, 호출 그래프, Swift와 Objective-C 탐색 | +| 네이티브 바이너리 | 10 | macOS의 Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 심볼과 LLDB 호출 관찰; Linux의 ELF 레이아웃, 정적 보호 증거와 기록된 크래시 | +| 아티팩트 그래프 | 7 | 디렉터리와 패키지 목록, 컴파일된 Interface Builder 파일, Apple 애셋 카탈로그, Mach-O dylib 로드 해석, 추출, 오프라인 EVM 인터페이스 추론 | +| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 | +| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 | +| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 | +| 브라우저 관찰 | 12 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 | +| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 | +| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 | +| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 | +| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 | ## 로드맵 @@ -233,7 +237,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin "mcpServers": { "rea": { "command": "npx", - "args": ["-y", "rea-agents@4.1.0", "mcp"] + "args": ["-y", "rea-agents@6.0.0", "mcp"] } } } @@ -273,7 +277,7 @@ rea --help rea mcp ``` -REA는 Mac의 `.app` 폴더를 직접 열 수 있습니다. 에이전트가 앱을 찾지 못하면 설치 위치를 알려 주세요. +REA는 macOS의 `.app` 폴더를 직접 열 수 있습니다. 에이전트가 앱을 찾지 못하면 설치 위치를 알려 주세요. ## Hopper 앱 동작 diff --git a/README_zh.md b/README_zh.md index 5395a5bf96..dea70c179f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,6 +15,10 @@ [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) [![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM) +🎉 **GitHub Star 突破 20,000,感谢大家!** + +感谢每一位使用 REA、反馈问题、测试构建和贡献修复的朋友。 + morluto%2Frea | Trendshift **[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)** @@ -138,7 +142,7 @@ rea setup ### 系统要求 - macOS 12 或更高版本 -- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux +- Ubuntu 24.04+、Fedora 41+、64 位 Arch Linux 或 CachyOS - Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm 原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。 @@ -153,7 +157,7 @@ Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、 运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。 -Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。 +Linux 上 REA 会优先使用可执行的 `/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)。 ### 更新与卸载 @@ -202,20 +206,20 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常 ## 调查工具目录 -| 工具类别 | 数量 | 用途 | -| ----------------- | ---: | ------------------------------------------------------------------------------------------------------ | -| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 | -| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 | -| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper | -| 产物图 | 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 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 | -| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 | +| 工具类别 | 数量 | 用途 | +| ----------------- | ---: | -------------------------------------------------------------------------------------------------------------------- | +| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 | +| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 | +| 原生二进制工具 | 10 | macOS 的 Mach-O 元数据、签名、plist、架构、Swift 符号与 LLDB 调用观察;Linux 的 ELF 布局、静态保护证据与历史崩溃记录 | +| 产物图 | 7 | 目录与软件包清单、编译后的 Interface Builder 文件、Apple 资源目录、Mach-O dylib 加载解析、提取与离线 EVM 接口推断 | +| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 | +| 固件 | 2 | Linux 固件区域检查与显式提取 | +| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 | +| 浏览器观察 | 12 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 | +| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 | +| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 | +| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 | +| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 | ## 路线图 @@ -232,7 +236,7 @@ Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsu "mcpServers": { "rea": { "command": "npx", - "args": ["-y", "rea-agents@4.1.0", "mcp"] + "args": ["-y", "rea-agents@6.0.0", "mcp"] } } } diff --git a/bridge/mitmproxy/capture.py b/bridge/mitmproxy/capture.py new file mode 100644 index 0000000000..fa0535c027 --- /dev/null +++ b/bridge/mitmproxy/capture.py @@ -0,0 +1,208 @@ +"""REA offline adapter using mitmproxy's unchanged native tnetstring decoder. + +No FlowReader migrations, HAR conversion, proxy listener, or target requests. +The containing mitmdump process and its configuration directory are REA-owned. +""" + +import base64 +import hashlib +import json +import math +import re +import resource +from pathlib import Path + +from mitmproxy import ctx, version +from mitmproxy.io import tnetstring + +PROFILE = "12.2.3" +OUTPUT_BYTES = 96 * 1024 * 1024 +CREDENTIALS = {b"authorization", b"proxy-authorization", b"cookie", b"set-cookie"} + + +class CaptureFailure(Exception): + def __init__(self, reason, message, pointer=""): + self.reason, self.pointer = reason, pointer + super().__init__(message) + + +def pointer(parent, key): + return parent + "/" + str(key).replace("~", "~0").replace("/", "~1") + + +class BoundedReader: + """Refuse malicious declared lengths before the upstream parser allocates.""" + def __init__(self, handle, size): + self.handle, self.size = handle, size + + def read(self, count): + if count < 0 or count > self.size - self.handle.tell(): + raise CaptureFailure("format", "Native record declares bytes beyond the retained capture.") + return self.handle.read(count) + + +def project_record(state, secrets): + binaries, numbers, redactions = [], [], [] + byte_secrets = [] + for secret in secrets: + try: + byte_secrets.append(secret.encode("utf-8", errors="strict")) + except UnicodeEncodeError: + # Non-scalar JSON strings have no UTF-8 byte representation. + # Keep them in secrets for the ordinary string projection. + pass + + def redact(text, path, original): + if any(secret in text or secret in original for secret in secrets): + redactions.append({"pointer": path, "reason": "explicit-sensitive-value"}) + return None + return text + + def transport_value(value, path, transport_url): + if not (transport_url or re.fullmatch(r"/(?:backup/)*request/(?:path|authority)", path)): + return value, False + binary = isinstance(value, bytes) + pattern = rb"^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?//)[^/?#]*@" if binary else r"^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?//)[^/?#]*@" + replacement = rb"\1" if binary else r"\1" + separator = b"@" if binary else "@" + safe_url = re.sub(pattern, replacement, value, flags=re.I) + if path.endswith("/authority") and separator in safe_url: + safe_url = safe_url.rsplit(separator, 1)[1] + changed = safe_url != value + if changed: + redactions.append({"pointer": path, "reason": "transport-credential"}) + return safe_url, changed + + def validate_omitted(value, path, depth): + before = len(binaries), len(numbers), len(redactions) + visit(value, path, depth) + del binaries[before[0]:] + del numbers[before[1]:] + del redactions[before[2]:] + + def visit(value, path, depth=0, credential=False, transport_url=False): + if depth > 64: + raise CaptureFailure("input-limit", "Native capture exceeds the 64-level evidence nesting budget.", path) + if credential: + validate_omitted(value, path, depth) + redactions.append({"pointer": path, "reason": "transport-credential"}) + if isinstance(value, bytes): + binaries.append({"pointer": path, "representation": "producer-bytes", "state": "redacted", "content_base64": None, "bytes": None, "sha256": None}) + return None + if isinstance(value, bytes): + safe_bytes, credential_url = transport_value(value, path, transport_url) + content_base64 = base64.b64encode(value).decode("ascii") + digest = hashlib.sha256(value).hexdigest() + hidden = any(secret in value or secret in safe_bytes for secret in byte_secrets) or any(secret in content_base64 or secret in digest for secret in secrets) + safe_text = None + try: + safe_text = safe_bytes.decode("utf-8", errors="strict") + except UnicodeDecodeError: + pass + if hidden: + redactions.append({"pointer": path, "reason": "explicit-sensitive-value"}) + exclude = hidden or credential_url + binaries.append({"pointer": path, "representation": "producer-bytes", "state": "redacted" if exclude else "retained", "content_base64": None if exclude else content_base64, "bytes": None if exclude else len(value), "sha256": None if exclude else digest}) + if hidden: + return None + return safe_text + if isinstance(value, str): + safe_text, _ = transport_value(value, path, transport_url) + return redact(safe_text, path, value) + if isinstance(value, bool) or value is None: + return value + if isinstance(value, (int, float)): + literal = repr(value) + if any(secret in literal for secret in secrets): + redactions.append({"pointer": path, "reason": "explicit-sensitive-value"}) + return None + numbers.append({"pointer": path, "producer_type": "integer" if isinstance(value, int) else "float", "literal": literal}) + return value if (abs(value) <= 9007199254740991 if isinstance(value, int) else math.isfinite(value)) else None + if isinstance(value, (list, tuple)): + fields = re.fullmatch(r"/(?:backup/)*(?:request|response)/(?:headers|trailers)", path) + if fields: + result = [] + for index, field in enumerate(value): + if not isinstance(field, (list, tuple)) or len(field) != 2 or not isinstance(field[0], bytes): + raise CaptureFailure("format", "Native header/trailer is not an ordered byte pair.", pointer(path, index)) + result.append([visit(field[0], pointer(pointer(path, index), 0), depth + 2), visit(field[1], pointer(pointer(path, index), 1), depth + 2, field[0].lower() in CREDENTIALS, field[0].lower() in {b"location", b"referer", b"origin"})]) + return result + return [visit(item, pointer(path, index), depth + 1) for index, item in enumerate(value)] + if isinstance(value, dict): + result = {} + for key, item in value.items(): + if not isinstance(key, str): + raise CaptureFailure("format", "Native record has a non-string dictionary key.", path) + if any(secret in key for secret in secrets): + # Validate the omitted subtree too, then discard all of its + # coordinates rather than inventing a replacement identity. + validate_omitted(item, pointer(path, key), depth + 1) + redactions.append({"pointer": path, "reason": "explicit-sensitive-value", "scope": "property-name"}) + continue + result[key] = visit(item, pointer(path, key), depth + 1) + return result + raise CaptureFailure("decoder", "Native parser returned an unexpected value type: " + type(value).__name__ + ".", path) + + return {"reported": visit(state, ""), "binary_fields": binaries, "numeric_literals": numbers, "redactions": redactions} + + +def decode(request): + if version.VERSION != PROFILE: + raise CaptureFailure("unsupported", "Expected mitmproxy " + PROFILE + "; observed " + version.VERSION + ".") + records = [] + capture = Path(request["snapshot_path"]) + size = capture.stat().st_size + with capture.open("rb") as handle: + reader = BoundedReader(handle, size) + while handle.tell() < size: + start = handle.tell() + try: + state = tnetstring.load(reader) + except CaptureFailure: + raise + except RecursionError: + raise CaptureFailure("input-limit", "Native parser exceeded its recursion budget at byte offset " + str(start) + ".") from None + except (ValueError, IndexError, UnicodeError, EOFError): + raise CaptureFailure("format", "Malformed native record at byte offset " + str(start) + ".") from None + if not isinstance(state, dict): + raise CaptureFailure("format", "Native flow record must be a dictionary at byte offset " + str(start) + ".") + records.append({"ordinal": len(records), "location": {"kind": "byte-range", "offset": start, "bytes": handle.tell() - start}, **project_record(state, request["sensitive_values"]), "limitations": ["Fields are original native states without FlowReader migration. Binary fields contain exact producer bytes; reported UTF-8 strings for those fields are derived display views. Unknown flow/version extensions are retained without interpretation."]}) + return {"decoder": {"id": "mitmproxy-native-tnetstring", "name": "REA offline native mitmproxy adapter", "version": PROFILE}, "container": {"reported": None, "numeric_literals": [], "redactions": [], "records_pointer": None}, "total_records": len(records), "records": records} + + +def failure_reply(error): + """Keep producer constraints, resource exhaustion and adapter defects distinct.""" + if isinstance(error, CaptureFailure): + return {"ok": False, "reason": error.reason, "message": str(error), "pointer": error.pointer} + if isinstance(error, MemoryError): + return {"ok": False, "reason": "resource-limit", "message": "Native decoder exhausted its 768 MiB address-space budget.", "pointer": ""} + return {"ok": False, "reason": "decoder", "message": "Offline native decoder failed unexpectedly: " + type(error).__name__ + ".", "pointer": ""} + + +class OfflineCapture: + def load(self, loader): + loader.add_option("rea_request_path", str, "", "REA private offline capture request") + + def running(self): + resource.setrlimit(resource.RLIMIT_AS, (768 * 1024 * 1024, 768 * 1024 * 1024)) + resource.setrlimit(resource.RLIMIT_CPU, (30, 30)) + resource.setrlimit(resource.RLIMIT_FSIZE, (OUTPUT_BYTES, OUTPUT_BYTES)) + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + request = json.loads(Path(ctx.options.rea_request_path).read_text(encoding="utf-8")) + try: + response = {"ok": True, "value": decode(request)} + except Exception as error: + response = failure_reply(error) + try: + try: + output = json.dumps(response, ensure_ascii=True, allow_nan=False, separators=(",", ":")).encode("ascii") + except Exception as error: + output = json.dumps(failure_reply(error), separators=(",", ":")).encode("ascii") + if len(output) > OUTPUT_BYTES: + output = b'{"ok":false,"reason":"limit","message":"Native reply exceeds the complete-evidence output budget.","pointer":""}' + Path(request["reply_path"]).write_bytes(output) + finally: + ctx.master.shutdown() + + +addons = [OfflineCapture()] diff --git a/bridge/native/NativeUIChildren.swift b/bridge/native/NativeUIChildren.swift index 8e2afcf9a5..10ac1d1749 100644 --- a/bridge/native/NativeUIChildren.swift +++ b/bridge/native/NativeUIChildren.swift @@ -1,8 +1,24 @@ +import Foundation + struct ChildBatch { let values: [Element] let complete: Bool } +struct ChildCount { + let value: Int? + let error: Int32? +} + +func serializeHelperOutput(_ value: Any) throws -> Data { + try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .withoutEscapingSlashes]) +} + +func captureChildCount(status: Int32, success: Int32, value: Int) -> ChildCount { + guard status == success else { return ChildCount(value: nil, error: status) } + return ChildCount(value: value, error: nil) +} + func captureChildValues(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch { guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) } let returned = copy() ?? [] diff --git a/bridge/native/ReaNativeUI.swift b/bridge/native/ReaNativeUI.swift index 05a24ccf52..2e4cc83414 100644 --- a/bridge/native/ReaNativeUI.swift +++ b/bridge/native/ReaNativeUI.swift @@ -22,9 +22,10 @@ func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? { var value: CFTypeRef? return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil } -func childCount(_ element: AXUIElement) -> Int { +func childCount(_ element: AXUIElement) -> ChildCount { var count = 0 - return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0 + let status = AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) + return captureChildCount(status: status.rawValue, success: AXError.success.rawValue, value: count) } func children(_ element: AXUIElement, count: Int) -> ChildBatch { captureChildValues(requestedCount: count) { @@ -88,7 +89,11 @@ func observe(_ request: Request) async throws -> [String: Any] { var element = root for index in action.path ?? [] { var selectedChild: CFArray? - guard index >= 0 && index < childCount(element), + let count = childCount(element) + guard let available = count.value else { + try fail("element-count-unavailable", "Cannot validate selected accessibility path because child count failed with AXError \(count.error ?? -1)") + } + guard index >= 0 && index < available, AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success, let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") } element = item @@ -104,15 +109,20 @@ func observe(_ request: Request) async throws -> [String: Any] { } guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") } } - var nodes: [[String: Any]] = []; var truncated = false + var nodes: [[String: Any]] = []; var truncated = false; var gaps: [String] = [] if request.accessibility, let root = selected { var pending: [(AXUIElement, [Int])] = [(root, [])] while let (element, path) = pending.popLast() { if nodes.count >= request.max_nodes { truncated = true; break } - let totalChildren = childCount(element) + let childCountResult = childCount(element) var actions: CFArray? AXUIElementCopyActionNames(element, &actions) - nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": totalChildren]) + nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": childCountResult.value.map { $0 as Any } ?? NSNull()]) + guard let totalChildren = childCountResult.value else { + truncated = true + gaps.append("AX child count unavailable at path \(path): AXError \(childCountResult.error ?? -1)") + continue + } if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue } let available = max(0, request.max_nodes - nodes.count - pending.count) let count = min(available, totalChildren) @@ -143,18 +153,18 @@ func observe(_ request: Request) async throws -> [String: Any] { } screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height] } - return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]] + return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? gaps : ["Accessibility capture was disabled"]] } Task { @MainActor in do { let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8)) let result = try await observe(request) - let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys]) + let output = try serializeHelperOutput(["ok": true, "result": result]) FileHandle.standardOutput.write(output) } catch { let failure = error as? BoundaryFailure let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription] - if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) } + if let data = try? serializeHelperOutput(output) { FileHandle.standardOutput.write(data) } } exit(0) } diff --git a/bridge/native/rea_lldb_tracer.py b/bridge/native/rea_lldb_tracer.py new file mode 100644 index 0000000000..306a30e593 --- /dev/null +++ b/bridge/native/rea_lldb_tracer.py @@ -0,0 +1,303 @@ +"""Bounded native call observation executed inside LLDB's embedded Python. + +REA imports this module into `lldb --batch` and runs `rea_trace `. +It launches one owned process, stops at the requested functions and +Objective-C methods, records each entry from registers and memory reads only, +continues, and kills the process once it exits, the event limit is reached or +the duration elapses. No expression is evaluated and no target code runs on +the tracer's behalf. The result is written as JSON to the configured path. +""" + +import json +import time + +import lldb + +ARGUMENT_REGISTERS = { + "arm64": ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"], + "arm64e": ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"], + "x86_64": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"], + "x86_64h": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"], +} +MAX_SELECTOR_BYTES = 1024 +MAX_REPORTED_LOCATIONS = 64 +MAX_OTHER_STOPS = 256 +LISTENER_NAME = "rea-native-call-observation" + + +def _hex(value): + return "0x%x" % value + + +def _location(address, target): + """Load and file address, module and symbol of one code address.""" + module = address.GetModule() + symbol = address.GetSymbol() + file_address = address.GetFileAddress() + return { + "load_address": _hex(address.GetLoadAddress(target)), + "file_address": None + if file_address == lldb.LLDB_INVALID_ADDRESS + else _hex(file_address), + "module": module.GetFileSpec().GetFilename() if module.IsValid() else None, + "module_path": str(module.GetFileSpec()) if module.IsValid() else None, + "symbol": symbol.GetName() if symbol.IsValid() else None, + } + + +def _no_files(): + return lldb.SBFileSpecList() + + +def _create_breakpoints(target, spec): + """LLDB breakpoints for one request; a class-qualified method may need two.""" + if spec["kind"] == "function": + modules = lldb.SBFileSpecList() + if spec.get("module"): + modules.Append(lldb.SBFileSpec(spec["module"], False)) + return [ + target.BreakpointCreateByName( + spec["name"], lldb.eFunctionNameTypeAuto, modules, _no_files() + ) + ] + class_name = spec.get("class_name") + method_type = spec.get("method_type", "any") + if class_name is None: + return [ + target.BreakpointCreateByName( + spec["selector"], lldb.eFunctionNameTypeSelector, _no_files(), _no_files() + ) + ] + prefixes = {"instance": ["-"], "class": ["+"], "any": ["-", "+"]}[method_type] + return [ + target.BreakpointCreateByName( + "%s[%s %s]" % (prefix, class_name, spec["selector"]), + lldb.eFunctionNameTypeFull, + _no_files(), + _no_files(), + ) + for prefix in prefixes + ] + + +def _dynamic_class(target, pointer): + """Class of an Objective-C receiver from LLDB's runtime reader, without running code.""" + if pointer == 0: + return None + data = lldb.SBData.CreateDataFromUInt64Array( + target.GetByteOrder(), target.GetAddressByteSize(), [pointer] + ) + value = target.CreateValueFromData( + "receiver", data, target.GetBasicType(lldb.eBasicTypeObjCID) + ) + dynamic = value.GetDynamicValue(lldb.eDynamicDontRunTarget) + name = dynamic.GetTypeName() if dynamic.IsValid() else None + if not name or name in ("id", "objc_object *"): + return None + return name[:-2] if name.endswith(" *") else name + + +def _wanted(spec, symbol): + """Selector-only requests may admit only instance or only class methods.""" + if spec["kind"] != "objc-method" or spec.get("class_name") is not None: + return True + method_type = spec.get("method_type", "any") + if method_type == "any": + return True + return symbol.startswith("-[" if method_type == "instance" else "+[") + + +def _record(context, thread, index, sequence): + target, process, config = context["target"], context["process"], context["config"] + frame = thread.GetFrameAtIndex(0) + location = _location(frame.GetPCAddress(), target) + symbol = location["symbol"] or "" + if not _wanted(config["breakpoints"][index], symbol): + return None + names = ARGUMENT_REGISTERS.get(config["architecture"], []) + registers = [ + {"name": name, "value": _hex(frame.FindRegister(name).GetValueAsUnsigned())} + for name in names[: config["argument_registers"]] + ] + receiver_class = None + selector = None + if symbol.startswith("-[") or symbol.startswith("+["): + error = lldb.SBError() + text = process.ReadCStringFromMemory( + frame.FindRegister(names[1]).GetValueAsUnsigned(), + MAX_SELECTOR_BYTES, + error, + ) + selector = text if error.Success() and text else None + if symbol.startswith("-["): + receiver_class = _dynamic_class( + target, frame.FindRegister(names[0]).GetValueAsUnsigned() + ) + frames = min(thread.GetNumFrames(), 1 + config["backtrace_frames"]) + record = { + "sequence": sequence, + "elapsed_ms": round((time.monotonic() - context["started"]) * 1000, 3), + "thread_id": thread.GetThreadID(), + "breakpoint_index": index, + "receiver_class": receiver_class, + "selector": selector, + "registers": registers, + "backtrace": [ + _location(thread.GetFrameAtIndex(depth).GetPCAddress(), target) + for depth in range(1, frames) + ], + } + record.update(location) + return record + + +def _handle_stop(context, events, other_stops): + """Record breakpoint hits of one stop; return an outcome when tracing must end.""" + process = context["process"] + for thread in process: + reason = thread.GetStopReason() + if reason == lldb.eStopReasonBreakpoint: + index = context["breakpoint_index"].get(thread.GetStopReasonDataAtIndex(0)) + if index is None: + continue + record = _record(context, thread, index, len(events)) + if record is not None: + events.append(record) + if len(events) >= context["config"]["max_events"]: + return "event-limit" + elif reason in (lldb.eStopReasonSignal, lldb.eStopReasonException): + other_stops.append(thread.GetStopDescription(256)) + if len(other_stops) >= MAX_OTHER_STOPS: + return "stop-limit" + return None + + +def _breakpoint_report(target, created): + report = [] + for index, breakpoints in enumerate(created): + locations = [] + count = 0 + for breakpoint in breakpoints: + count += breakpoint.GetNumLocations() + for position in range(breakpoint.GetNumLocations()): + if len(locations) < MAX_REPORTED_LOCATIONS: + locations.append( + _location(breakpoint.GetLocationAtIndex(position).GetAddress(), target) + ) + report.append({"index": index, "location_count": count, "locations": locations}) + return report + + +def _launch_info(target, config, listener): + launch = lldb.SBLaunchInfo(config["arguments"]) + launch.SetListener(listener) + environment = target.GetEnvironment() + for key, value in config["environment"].items(): + environment.Set(key, value, True) + launch.SetEnvironment(environment, False) + if config.get("working_directory"): + launch.SetWorkingDirectory(config["working_directory"]) + launch.AddOpenFileAction(0, "/dev/null", True, False) + launch.AddOpenFileAction(1, config["stdout_path"], False, True) + launch.AddOpenFileAction(2, config["stderr_path"], False, True) + return launch + + +def _wait_for_exit(listener, process, seconds): + event = lldb.SBEvent() + deadline = time.monotonic() + seconds + while process.GetState() != lldb.eStateExited and time.monotonic() < deadline: + listener.WaitForEvent(1, event) + + +def trace(debugger, config): + debugger.SetAsync(True) + # Stop at the symbol itself, where argument registers still hold the ABI + # arguments, even when line tables would let LLDB skip the prologue. + lldb.SBDebugger.SetInternalVariable( + "target.skip-prologue", "false", debugger.GetInstanceName() + ) + error = lldb.SBError() + target = debugger.CreateTarget( + config["executable"], config["architecture"], None, False, error + ) + if not target.IsValid(): + return {"status": "target-error", "error": error.GetCString()} + created = [_create_breakpoints(target, spec) for spec in config["breakpoints"]] + breakpoint_index = {} + for index, breakpoints in enumerate(created): + for breakpoint in breakpoints: + breakpoint_index[breakpoint.GetID()] = index + listener = lldb.SBListener(LISTENER_NAME) + started = time.monotonic() + process = target.Launch(_launch_info(target, config, listener), error) + if not error.Success() or not process.IsValid(): + return {"status": "launch-error", "error": error.GetCString()} + with open(config["pid_path"], "w") as handle: + handle.write(str(process.GetProcessID())) + context = { + "target": target, + "process": process, + "config": config, + "started": started, + "breakpoint_index": breakpoint_index, + } + events = [] + other_stops = [] + outcome = None + deadline = started + config["duration_ms"] / 1000 + event = lldb.SBEvent() + while outcome is None: + if time.monotonic() >= deadline: + outcome = "duration-elapsed" + break + if not listener.WaitForEvent(1, event): + continue + if not lldb.SBProcess.EventIsProcessEvent(event): + continue + state = lldb.SBProcess.GetStateFromEvent(event) + if state == lldb.eStateExited: + outcome = "exited" + elif state == lldb.eStateStopped and not lldb.SBProcess.GetRestartedFromEvent(event): + outcome = _handle_stop(context, events, other_stops) + if outcome is None: + process.Continue() + pid = process.GetProcessID() + exit_status = None + exit_description = None + if outcome == "exited": + exit_status = process.GetExitStatus() + exit_description = process.GetExitDescription() + else: + process.Kill() + _wait_for_exit(listener, process, 5) + return { + "status": "traced", + "version": lldb.SBDebugger.GetVersionString().splitlines()[0], + "pid": pid, + "outcome": outcome, + "exit_status": exit_status, + "exit_description": exit_description, + "killed": outcome != "exited", + "terminated": process.GetState() == lldb.eStateExited, + "elapsed_ms": round((time.monotonic() - started) * 1000, 3), + "breakpoints": _breakpoint_report(target, created), + "events": events, + "other_stops": other_stops, + } + + +def run(debugger, command, result, internal_dict): + """LLDB command entry point: `rea_trace `.""" + with open(command.strip()) as handle: + config = json.load(handle) + try: + output = trace(debugger, config) + except Exception as error: # Reported as a tracer failure, never re-raised into LLDB. + output = {"status": "tracer-error", "error": "%s: %s" % (type(error).__name__, error)} + with open(config["result_path"], "w") as handle: + json.dump(output, handle) + + +def __lldb_init_module(debugger, internal_dict): + debugger.HandleCommand("command script add -f rea_lldb_tracer.run rea_trace") diff --git a/bridge/process/ProcessRunTokenReader.swift b/bridge/process/ProcessRunTokenReader.swift new file mode 100644 index 0000000000..1438f81577 --- /dev/null +++ b/bridge/process/ProcessRunTokenReader.swift @@ -0,0 +1,293 @@ +import Darwin +import Foundation + +struct RunTokenObservation: Encodable { + let pid: Int32 + let state: String + let run_id: String? + let reason: String? +} + +struct ProcessIdentityObservation: Encodable { + let pid: Int32 + let state: String + let identity: String? + let reason: String? +} + +private struct ProcessMetadata { + let identity: String + let pointerSize: Int +} + +struct ProcessSnapshot { + fileprivate let metadata: [Int32: ProcessMetadata] + let failure: String? +} + +typealias ProcessTableSysctlCall = ( + _ mib: inout [Int32], + _ output: UnsafeMutableRawPointer?, + _ size: inout Int +) -> Int32 + +private func readProcessSnapshot() -> ProcessSnapshot { + readProcessSnapshot { mib, output, size in + sysctl(&mib, u_int(mib.count), output, &size, nil, 0) + } +} + +func readProcessSnapshot( + sysctlCall: ProcessTableSysctlCall +) -> ProcessSnapshot { + for _ in 0..<3 { + var mib = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0] + var size = 0 + guard sysctlCall(&mib, nil, &size) == 0 else { + let failure = errno + return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(failure)") + } + guard size >= MemoryLayout.size, + size % MemoryLayout.stride == 0 else { + return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(errno)") + } + var processes = [kinfo_proc]( + repeating: kinfo_proc(), + count: size / MemoryLayout.stride + ) + let result = processes.withUnsafeMutableBufferPointer { buffer in + let output = buffer.baseAddress.map { UnsafeMutableRawPointer($0) } + return sysctlCall(&mib, output, &size) + } + guard result == 0 else { + let failure = errno + if failure == ENOMEM { continue } + return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(failure)") + } + guard size % MemoryLayout.stride == 0 else { + return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(errno)") + } + let processCount = size / MemoryLayout.stride + let metadata = Dictionary( + processes.prefix(processCount).map { process in + let start = process.kp_proc.p_starttime + return ( + process.kp_proc.p_pid, + ProcessMetadata( + identity: "\(start.tv_sec):\(start.tv_usec)", + pointerSize: process.kp_proc.p_flag & P_LP64 != 0 ? 8 : 4 + ) + ) + }, + uniquingKeysWith: { first, _ in first } + ) + return ProcessSnapshot(metadata: metadata, failure: nil) + } + return ProcessSnapshot(metadata: [:], failure: "process_table_failed_\(ENOMEM)") +} + +func readProcessIdentities(pids: [Int32]) -> [ProcessIdentityObservation] { + let snapshot = readProcessSnapshot() + return pids.map { pid in + guard let metadata = snapshot.metadata[pid] else { + return unavailableIdentity( + pid: pid, + reason: snapshot.failure ?? "process_not_in_snapshot" + ) + } + return ProcessIdentityObservation(pid: pid, state: "readable", identity: metadata.identity, reason: nil) + } +} + +private func unavailableIdentity(pid: Int32, reason: String) -> ProcessIdentityObservation { + ProcessIdentityObservation(pid: pid, state: "unavailable", identity: nil, reason: reason) +} + +enum RunTokenReadError: Error { + case invalidBuffer + case duplicateToken + case invalidTokenEncoding + case environmentUnavailable + case appleVectorUnavailable + case ambiguousEnvironmentBoundary +} + +@_silgen_name("csops") +private func csops( + _ pid: Int32, + _ ops: UInt32, + _ addr: UnsafeMutableRawPointer?, + _ size: Int +) -> Int32 + +private let csOpsStatus: UInt32 = 0 +private let csPlatformBinary: UInt32 = 0x0400_0000 + +/** + * Whether the target image is an Apple platform binary. Current macOS omits + * the environment of platform binaries from KERN_PROCARGS2 for every caller, + * so this distinguishes a policy-withheld environment from a process whose + * environment is genuinely absent or transiently unreadable. + */ +func isPlatformBinary(pid: Int32) -> Bool { + var flags: UInt32 = 0 + let result = withUnsafeMutablePointer(to: &flags) { pointer in + csops(pid, csOpsStatus, UnsafeMutableRawPointer(pointer), MemoryLayout.size) + } + return result == 0 && (flags & csPlatformBinary) != 0 +} + +private func readRunToken(pid: Int32, pointerSize: Int) -> RunTokenObservation { + var mib = [CTL_KERN, KERN_PROCARGS2, Int32(pid)] + var size = 0 + guard sysctl(&mib, u_int(mib.count), nil, &size, nil, 0) == 0, size >= MemoryLayout.size else { + return unavailable(pid: pid, reason: "sysctl_failed_\(errno)") + } + var bytes = [UInt8](repeating: 0, count: size) + let result = bytes.withUnsafeMutableBytes { buffer in + sysctl(&mib, u_int(mib.count), buffer.baseAddress, &size, nil, 0) + } + guard result == 0 else { + return unavailable(pid: pid, reason: "sysctl_failed_\(errno)") + } + bytes = Array(bytes.prefix(size)) + do { + return RunTokenObservation(pid: pid, state: "readable", run_id: try token(in: bytes, pointerSize: pointerSize), reason: nil) + } catch RunTokenReadError.duplicateToken { + return unavailable(pid: pid, reason: "duplicate_run_token") + } catch RunTokenReadError.invalidTokenEncoding { + return unavailable(pid: pid, reason: "invalid_run_token_encoding") + } catch RunTokenReadError.environmentUnavailable { + return unavailable( + pid: pid, + reason: isPlatformBinary(pid: pid) + ? "platform_binary_environment_withheld" + : "environment_unavailable" + ) + } catch RunTokenReadError.appleVectorUnavailable { + return unavailable(pid: pid, reason: "apple_vector_unavailable") + } catch RunTokenReadError.ambiguousEnvironmentBoundary { + return unavailable(pid: pid, reason: "ambiguous_environment_boundary") + } catch { + return unavailable(pid: pid, reason: "malformed_procargs") + } +} + +func readRunTokens(pids: [Int32]) -> [RunTokenObservation] { + let before = readProcessSnapshot() + let observations = pids.map { pid -> RunTokenObservation in + guard let metadata = before.metadata[pid] else { + return unavailable(pid: pid, reason: before.failure ?? "process_not_in_snapshot") + } + return readRunToken(pid: pid, pointerSize: metadata.pointerSize) + } + let after = readProcessSnapshot() + return zip(pids, observations).map { pid, observation in + guard let initial = before.metadata[pid], let current = after.metadata[pid] else { + return unavailable(pid: pid, reason: after.failure ?? "process_not_in_snapshot") + } + guard initial.identity == current.identity, + initial.pointerSize == current.pointerSize else { + return unavailable(pid: pid, reason: "process_identity_changed_during_token_read") + } + return observation + } +} + +func token(in bytes: [UInt8], pointerSize: Int = MemoryLayout.size) throws -> String? { + guard bytes.count >= MemoryLayout.size else { throw RunTokenReadError.invalidBuffer } + let argc = bytes.withUnsafeBytes { $0.loadUnaligned(as: Int32.self).littleEndian } + guard argc >= 0, Int(argc) <= bytes.count else { throw RunTokenReadError.invalidBuffer } + var offset = MemoryLayout.size + guard let executablePath = readString(in: bytes, offset: &offset) else { + throw RunTokenReadError.invalidBuffer + } + guard pointerSize == 4 || pointerSize == 8 else { throw RunTokenReadError.invalidBuffer } + let pathRecordSize = Array("executable_path=".utf8).count + executablePath.count + 1 + let padding = (pointerSize - pathRecordSize % pointerSize) % pointerSize + guard offset + padding <= bytes.count, + bytes[offset..<(offset + padding)].allSatisfy({ $0 == 0 }) else { + throw RunTokenReadError.invalidBuffer + } + offset += padding + for _ in 0..] = [] + var zeroedAppleVectorStart: Int? + while offset < bytes.count { + if bytes[offset] == 0 { + let start = offset + while offset < bytes.count, bytes[offset] == 0 { + offset += 1 + } + if offset - start > 1 { + zeroedAppleVectorStart = zeroedAppleVectorStart ?? trailingEntries.count + } + continue + } + guard let entry = readString(in: bytes, offset: &offset) else { + throw RunTokenReadError.invalidBuffer + } + trailingEntries.append(entry) + } + // XNU appends its Apple vector after the environment. libc clears the + // initial `pfz=` record after startup, leaving a multi-NUL run in its place. + // A caller can also set `pfz`, so prefer the final intact sentinel when it + // remains available and otherwise stop at the cleared record. + let intactAppleVectorStart = trailingEntries.lastIndex(where: { + $0.starts(with: Array("pfz=".utf8)) + }) + let preferZeroedBoundary = zeroedAppleVectorStart.flatMap { zeroedStart in + intactAppleVectorStart.map { intactStart in intactStart < zeroedStart } ?? true + } ?? false + let appleVectorStart = preferZeroedBoundary + ? zeroedAppleVectorStart + : intactAppleVectorStart + guard let appleVectorStart else { + throw RunTokenReadError.appleVectorUnavailable + } + if trailingEntries[appleVectorStart...].contains(where: { + $0.starts(with: Array("REA_PROCESS_RUN_ID=".utf8)) + }) { + throw RunTokenReadError.ambiguousEnvironmentBoundary + } + let environmentEntries = trailingEntries[.. OUTPUT_BYTES: + encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete debugger context exceeds64 MiB reply budget."}).encode("utf-8") + descriptor = os.open(request["reply_path"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "wb") as handle: + handle.write(encoded) + except MemoryError: + reserve = None + failure_exit(request_path, 75, b"M") + except OSError as error: + if error.errno == 27: + failure_exit(request_path, 76, b"F") + if error.errno == 12: + reserve = None + failure_exit(request_path, 75, b"M") + raise + + +main() diff --git a/bridge/pwndbg/launch.py b/bridge/pwndbg/launch.py new file mode 100644 index 0000000000..7439167e25 --- /dev/null +++ b/bridge/pwndbg/launch.py @@ -0,0 +1,39 @@ +"""Catch typed resource failures while loading or running the owned debugger launcher. + +Only this package's fixed sibling is executed; caller input stays in request.json. +Interpreter startup failures before this boundary remain unclassified. +""" +import os +import sys + + +def resource_exit(status, marker): + try: + if len(sys.argv) > 2: + descriptor = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + os.write(descriptor, marker) + os.close(descriptor) + except (OSError, MemoryError): + # Without a complete marker the parent keeps this failure unclassified. + pass + os._exit(status) + +try: + implementation = os.path.join(os.path.dirname(__file__), "launch_impl.py") + with open(implementation, "rb") as handle: + source = handle.read() + exec(compile(source, implementation, "exec"), { + "__file__": implementation, + "__name__": "__main__", + }) +except MemoryError: + # Reserved status: an observed allocation failure, including imports/compile. + resource_exit(75, b"M") +except OSError as error: + # Linux ENOMEM is an observed allocation failure, independent of error text. + if error.errno == 12: + resource_exit(75, b"M") + # Linux EFBIG is observed directly, including a partial limits/reply write. + if error.errno == 27: + resource_exit(76, b"F") + raise diff --git a/bridge/pwndbg/launch_impl.py b/bridge/pwndbg/launch_impl.py new file mode 100644 index 0000000000..c1cd04b04d --- /dev/null +++ b/bridge/pwndbg/launch_impl.py @@ -0,0 +1,34 @@ +"""Lower inherited limits before replacing this owned process with GDB.""" +import json +import os +from pathlib import Path +import sys + +runtime_path = Path(__file__).parent.parent / "pwntools" / "decoder_runtime.py" +runtime = {"__file__": str(runtime_path), "__name__": "rea_decoder_runtime"} +exec(compile(runtime_path.read_bytes(), str(runtime_path), "exec"), runtime) + + +class LaunchFailure(Exception): + def __init__(self, reason, message): + super().__init__(message) + self.reason = reason + + +def launch(snapshot, cache): + request_path = sys.argv[-1] + request = json.loads(Path(request_path).read_text(encoding="utf-8")) + bridge = str(Path(__file__).with_name("core_context.py")) + environment = dict(os.environ, REA_CORE_CONTEXT_REQUEST=request_path) + # Only fixed package code appears in the GDB Python command. Selected paths stay JSON data. + command = "python exec(compile(open(" + repr(bridge) + ", 'rb').read(), " + repr(bridge) + ", 'exec'), {'__name__':'__main__','__file__':" + repr(bridge) + "})" + try: + os.execve(request["gdb"], [request["gdb"], "-nx", "-nh", "--batch", "--quiet", + "-iex", "set auto-load off", "-iex", "set debuginfod enabled off", + "-iex", "set auto-solib-add off", "-ex", command], environment) + except OSError as error: + raise LaunchFailure("unavailable", "Selected GDB could not start (" + str(error.errno) + "): " + request["gdb"] + "; " + str(error)) from error + + +if __name__ == "__main__": + runtime["main"](sys.argv[-1], launch, LaunchFailure, "pwndbg-core-launcher") diff --git a/bridge/pwntools/decoder_runtime.py b/bridge/pwntools/decoder_runtime.py new file mode 100644 index 0000000000..6681dffee0 --- /dev/null +++ b/bridge/pwntools/decoder_runtime.py @@ -0,0 +1,59 @@ +"""Shared bounded reply/limit protocol, independent of the supplied artifact format.""" +import json +import os +from pathlib import Path +import resource +import sys + +OUTPUT_BYTES = 64 * 1024 * 1024 + +def lower_resource_limits(): + limits = {} + for name, kind, maximum in ( + ("address_space_bytes", resource.RLIMIT_AS, 3 * 1024**3), + ("cpu_seconds", resource.RLIMIT_CPU, 30), + ("file_size_bytes", resource.RLIMIT_FSIZE, OUTPUT_BYTES), + ): + soft, hard = resource.getrlimit(kind) + finite = [maximum] + [value for value in (soft, hard) if value != resource.RLIM_INFINITY] + effective = min(finite) + # Preserve the inherited hard boundary and never raise a caller's soft limit. + resource.setrlimit(kind, (effective, hard)) + limits[name] = effective + return limits + + +def main(request_path, inspect, failure_type, profile): + request = json.loads(Path(request_path).read_text(encoding="utf-8")) + os.environ["PWNLIB_NOTERM"] = "1" + os.environ["PWNLIB_CACHE_DIR"] = str(Path(request_path).parent / "cache") + limits = None + # Release bounded emergency headroom before constructing a MemoryError reply. + # The outer exit-status contract also covers allocation failure during serialization. + memory_reserve = bytearray(1024 * 1024) + try: + limits = lower_resource_limits() + descriptor = os.open(Path(request_path).parent / "limits.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + json.dump(limits, handle) + value = inspect(Path(request["snapshot_path"]), Path(request_path).parent / "cache") + value["limitations"].append("Effective owned Python resource soft limits: " + json.dumps(limits, sort_keys=True) + ". Inherited tighter limits are retained.") + reply = {"ok": True, "profile": profile, "value": value} + except failure_type as error: + reply = {"ok": False, "reason": error.reason, "message": str(error)} + except MemoryError: + memory_reserve = None + reply = {"ok": False, "reason": "resource-limit", "message": "pwntools memory allocation failed under the effective resource limits; the exact allocation cause is unknown.", "reported_limits": limits} + except Exception as error: + if isinstance(error, OSError) and error.errno == 12: + memory_reserve = None + reply = {"ok": False, "reason": "resource-limit", "message": "pwntools reported OSError(ENOMEM); the exact allocation cause is unknown.", "reported_limits": limits} + else: + reply = {"ok": False, "reason": "decoder", "message": type(error).__name__ + ": " + str(error)} + encoded = json.dumps(reply, ensure_ascii=True, allow_nan=False).encode("utf-8") + if len(encoded) > OUTPUT_BYTES: + encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete artifact result exceeds the 64 MiB reply budget."}).encode("utf-8") + descriptor = os.open(request["reply_path"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "wb") as handle: + handle.write(encoded) + diff --git a/bridge/pwntools/layout.py b/bridge/pwntools/layout.py new file mode 100644 index 0000000000..0d39344ea3 --- /dev/null +++ b/bridge/pwntools/layout.py @@ -0,0 +1,39 @@ +"""Catch typed resource failures while loading or running the owned ELF adapter. + +Only this package's fixed sibling is executed; caller input stays in request.json. +Interpreter startup failures before this boundary remain unclassified. +""" +import os +import sys + + +def resource_exit(status, marker): + try: + if len(sys.argv) > 2: + descriptor = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + os.write(descriptor, marker) + os.close(descriptor) + except (OSError, MemoryError): + # Without a complete marker the parent keeps this failure unclassified. + pass + os._exit(status) + +try: + implementation = os.path.join(os.path.dirname(__file__), "layout_impl.py") + with open(implementation, "rb") as handle: + source = handle.read() + exec(compile(source, implementation, "exec"), { + "__file__": implementation, + "__name__": "__main__", + }) +except MemoryError: + # Reserved status: an observed allocation failure, including imports/compile. + resource_exit(75, b"M") +except OSError as error: + # Linux ENOMEM is an observed allocation failure, independent of error text. + if error.errno == 12: + resource_exit(75, b"M") + # Linux EFBIG is observed directly, including a partial limits/reply write. + if error.errno == 27: + resource_exit(76, b"F") + raise diff --git a/bridge/pwntools/layout_impl.py b/bridge/pwntools/layout_impl.py new file mode 100644 index 0000000000..0cae2b46f0 --- /dev/null +++ b/bridge/pwntools/layout_impl.py @@ -0,0 +1,309 @@ +"""Offline adapter to unchanged pwntools 4.15.0; never use libs/maps/libc. + +Owned request/snapshot/reply paths are supplied by REA. No host target-process launch, +user configuration initialization, debugger startup, or dependency installation. +""" +import base64 +import importlib.metadata +import io +import json +import os +from pathlib import Path +import resource +import sys + +PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2" +PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"} +OUTPUT_BYTES = 64 * 1024 * 1024 + + +class LayoutFailure(Exception): + def __init__(self, reason, message): + super().__init__(message) + self.reason = reason + + +def address(value): + return hex(int(value)) + + +def file_range(offset, size, length): + return 0 <= offset <= length and 0 <= size <= length - offset + + +def location(offset, size, length): + if not file_range(offset, size, length): + raise LayoutFailure("format", "ELF contains a file-backed range outside the selected snapshot.") + return {"offset": address(offset), "bytes": address(size)} + + +def name_reference(display, section, offset, content): + header = getattr(section, "header", None) + if header is None or header.sh_type != "SHT_STRTAB": + return {"display": display, "bytes_base64": None, "location": None, + "unknown_reason": "Referenced string table could not be resolved."} + return name_in_table(display, header.sh_offset, header.sh_size, offset, content) + + +def name_in_table(display, table_start, table_size, offset, content): + location(table_start, table_size, len(content)) + if not 0 <= offset < table_size: + raise LayoutFailure("format", f"ELF name offset {offset} lies outside its declared string table of {table_size} bytes at file offset {table_start}.") + start = table_start + offset + limit = table_start + table_size + end = content.find(b"\0", start, limit) + if end < 0: + raise LayoutFailure("format", "ELF name lacks a terminator within its reported string table.") + raw = content[start:end] + return {"display": raw.decode("utf-8", "replace") if display is None else display, "bytes_base64": base64.b64encode(raw).decode("ascii"), + "location": location(start, len(raw) + 1, len(content)), "unknown_reason": None} + + +def dynamic_name_reference(tags, offset, image, content): + starts = [tag.d_val for tag in tags if tag.d_tag == "DT_STRTAB"] + sizes = [tag.d_val for tag in tags if tag.d_tag == "DT_STRSZ"] + if not starts or not sizes: + raise LayoutFailure("format", "ELF dependency requires DT_STRTAB and DT_STRSZ.") + if len(starts) != 1 or len(sizes) != 1: + raise LayoutFailure("unsupported-target", "ELF dependency string-table tags are ambiguous: " + repr({"DT_STRTAB": starts, "DT_STRSZ": sizes})) + if not 0 <= offset < sizes[0]: + raise LayoutFailure("format", f"ELF dependency name offset {offset} lies outside declared DT_STRSZ {sizes[0]}.") + offsets = set() + if len(starts) == 1 and len(sizes) == 1: + for segment in image.iter_segments_by_type("PT_LOAD"): + h = segment.header + if starts[0] >= h.p_vaddr and starts[0] + sizes[0] <= h.p_vaddr + h.p_filesz: + offsets.add(h.p_offset + starts[0] - h.p_vaddr) + if len(offsets) != 1: + raise LayoutFailure("unsupported-target", "Dynamic string table lacks a unique file-backed mapping: " + repr({"DT_STRTAB": starts, "DT_STRSZ": sizes, "candidate_file_offsets": sorted(offsets)})) + return name_in_table(None, offsets.pop(), sizes[0], offset, content) + + +def symbol_value_meaning(raw, image_type, sections): + index = raw.st_shndx + if raw.st_info.type == "STT_FILE": return "no-address" + if index == "SHN_UNDEF": return "undefined" + if index == "SHN_COMMON": return "alignment" + if index == "SHN_ABS": return "absolute-value" + if not isinstance(index, int) or not 0 < index < len(sections) or index >= 0xff00: + return "unknown-section-index" + if sections[index].header.sh_type == "SHT_NULL": + return "unknown-section-index" + if image_type == "ET_REL": return "section-offset" + if raw.st_info.type == "STT_TLS": return "tls-offset" + return "linked-virtual-address" + + +def relocation_target(image_type, section_index, offset, sections): + if image_type != "ET_REL": + return {"kind": "linked-virtual-address", "address": address(offset)} + if section_index == 0 or sections[section_index].header.sh_type == "SHT_NULL": + return {"kind": "unknown-section", "reported_section_index": section_index, + "offset": address(offset), "unknown_reason": "undefined-section-reference" if section_index == 0 else "inactive-section-header"} + return {"kind": "section-offset", "section_index": section_index, "offset": address(offset)} + + +def inspect_elf(path, cache): + for package, expected in PACKAGES.items(): + try: + version = importlib.metadata.version(package) + except importlib.metadata.PackageNotFoundError as error: + raise LayoutFailure("unavailable", "Selected Python lacks " + package + " " + expected + ".") from error + if version != expected: + raise LayoutFailure("unsupported", "Expected " + package + " " + expected + "; selected Python reports " + version + ".") + content = path.read_bytes() + if len(content) < 4 or content[:4] != b"\x7fELF": + raise LayoutFailure("format", "Selected object does not contain an ELF header.") + if len(content) < 16: + raise LayoutFailure("format", "Selected ELF identification is truncated.") + if content[4] != 2 or content[5] != 1: + raise LayoutFailure("unsupported-target", "Initial layout profile requires ELF64 little-endian objects.") + if len(content) < 64: + raise LayoutFailure("format", "Selected ELF64 header is truncated.") + if int.from_bytes(content[18:20], "little") != 62: + raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF objects.") + if int.from_bytes(content[16:18], "little") not in (1, 2, 3): + raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.") + from pwnlib.context import context + with context.local(cache_dir=str(cache), log_level="warning"): + from elftools.common.exceptions import ELFError + from elftools.elf.elffile import ELFFile + from pwnlib.elf import ELF + try: + headers = ELFFile(io.BytesIO(content)) + section_count = headers.num_sections() + if headers.header.e_shstrndx == 0xffff and section_count == 0: + raise LayoutFailure("format", "ELF extended section-name index requires a declared section-zero header.") + raw_name_index = headers.header.e_shstrndx + if 0xff00 <= raw_name_index < 0xffff: + raise LayoutFailure("format", f"ELF section-name index {raw_name_index} is reserved and cannot directly reference a section.") + section_name_index = headers.get_shstrndx() + if raw_name_index == 0xffff and section_name_index < 0xff00: + raise LayoutFailure("format", f"ELF SHN_XINDEX resolves to ordinary index {section_name_index}; the escape requires an index at least SHN_LORESERVE (0xff00).") + if section_name_index != 0: + if section_name_index >= section_count: + raise LayoutFailure("format", f"ELF section-name table index {section_name_index} lies outside the declared section count {section_count}.") + names_header = headers._get_section_header(section_name_index) + if names_header.sh_type != "SHT_STRTAB": + raise LayoutFailure("format", f"ELF section-name table {section_name_index} is not SHT_STRTAB.") + location(names_header.sh_offset, names_header.sh_size, len(content)) + except ELFError as error: + raise LayoutFailure("format", str(error)) from error + class LayoutELF(ELF): + def _get_section_name(self, header): + # SHN_UNDEF declares no names; upstream would otherwise read + # strings at the inactive section-zero offset. + return "" if section_name_index == 0 else super()._get_section_name(header) + + # pyelftools 0.33 calls these documented filters internally; + # pwntools 4.15.0's cached overrides do not accept the argument. + # Forward the filter on this adapter instance, without modifying + # either upstream package or process-global methods. + def iter_segments(self, type=None): + return (segment for segment in super().iter_segments() + if type is None or segment.header.p_type == type) + + def iter_sections(self, type=None): + return (section for section in super().iter_sections() + if type is None or section.header.sh_type == type) + try: + image = LayoutELF(str(path), checksec=False) + if image.arch != "amd64" or image.bits != 64 or image.endian != "little": + raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF64 little-endian objects.") + if image.header.e_type not in ("ET_EXEC", "ET_DYN", "ET_REL"): + raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.") + length = len(content) + all_sections = list(image.iter_sections()) + shstrings = image.get_section(section_name_index) if all_sections and section_name_index != 0 else None + sections, segments, symbols, relocations, packed_relatives = [], [], [], [], [] + needed, interpreters = [], [] + for index, section in enumerate(all_sections): + h = section.header + backing = h.sh_type not in ("SHT_NOBITS", "SHT_NULL") + if backing: location(h.sh_offset, h.sh_size, length) + sections.append({ + "index": index, "name": name_reference(section.name, shstrings, h.sh_name, content) if shstrings is not None else {"display": "", "bytes_base64": None, "location": None, "unknown_reason": "ELF declares no section-name table (SHN_UNDEF index 0)."}, "name_offset": address(h.sh_name), "type": h.sh_type, + "header_location": location(image.header.e_shoff + index * image.header.e_shentsize, image.header.e_shentsize, length), + "address": address(h.sh_addr), "offset": address(h.sh_offset), + "size": address(h.sh_size), "alignment": address(h.sh_addralign), + "flags": address(h.sh_flags), "link": h.sh_link, "info": h.sh_info, + "entry_size": address(h.sh_entsize), "file_backing": "file" if backing else "none", + }) + if h.sh_type in ("SHT_SYMTAB", "SHT_DYNSYM"): + if h.sh_entsize < image.structs.Elf_Sym.sizeof(): + raise LayoutFailure("format", "ELF symbol entry is smaller than the decoded Elf64_Sym structure.") + if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type != "SHT_STRTAB": + raise LayoutFailure("format", f"ELF symbol table {index} links to invalid string table {h.sh_link}.") + strings = all_sections[h.sh_link] + for entry, symbol in enumerate(section.iter_symbols()): + raw = symbol.entry + symbols.append({ + "table_index": index, "entry_index": entry, + "name": name_reference(symbol.name, strings, raw.st_name, content), "name_offset": address(raw.st_name), + "location": location(h.sh_offset + entry * h.sh_entsize, h.sh_entsize, length), + "value": address(raw.st_value), "value_meaning": symbol_value_meaning(raw, image.header.e_type, all_sections), + "size": address(raw.st_size), "binding": raw.st_info.bind, "type": raw.st_info.type, + "visibility": raw.st_other.visibility, "section_index": raw.st_shndx, + }) + if h.sh_type in ("SHT_REL", "SHT_RELA"): + if image.header.e_type == "ET_REL" and h.sh_info >= len(all_sections): + raise LayoutFailure("format", f"ELF relocation section {index} references missing target section {h.sh_info}.") + symbol_table = None + if h.sh_link != 0: + if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type not in ("SHT_SYMTAB", "SHT_DYNSYM"): + raise LayoutFailure("format", f"ELF relocation section {index} links to invalid symbol table {h.sh_link}.") + symbol_table = all_sections[h.sh_link] + for entry, relocation in enumerate(section.iter_relocations()): + raw = relocation.entry + if raw.r_info_sym != 0 and (symbol_table is None or raw.r_info_sym >= symbol_table.num_symbols()): + raise LayoutFailure("format", f"ELF relocation section {index} entry {entry} references missing symbol {raw.r_info_sym} in table {h.sh_link}.") + relocations.append({ + "section_index": index, "entry_index": entry, "reported_offset": address(raw.r_offset), + "target": relocation_target(image.header.e_type, h.sh_info, raw.r_offset, all_sections), + "location": location(h.sh_offset + entry * h.sh_entsize, h.sh_entsize, length), + "type": raw.r_info_type, "symbol_table_index": h.sh_link, "symbol_index": raw.r_info_sym, + "symbol_reference_meaning": "zero-symbol-value" if raw.r_info_sym == 0 else "symbol-table-entry", + "addend": None if "r_addend" not in raw else str(raw.r_addend), + }) + if h.sh_type == "SHT_RELR": + packed_relatives.append({ + "section_index": index, + "location": location(h.sh_offset, h.sh_size, length), + "encoded_bytes_base64": base64.b64encode(content[h.sh_offset:h.sh_offset + h.sh_size]).decode("ascii"), + "entries": [{"decoded_index": decoded, "reported_offset": address(relocation.entry.r_offset)} for decoded, relocation in enumerate(section.iter_relocations())], + "offset_meaning": "unknown" if image.header.e_type == "ET_REL" else "linked-virtual-address", + "evidence_kind": "derived", "entry_source_locations": None, "addends": None, + }) + for index, segment in enumerate(image.iter_segments()): + h = segment.header + backing = h.p_type != "PT_NULL" and h.p_filesz != 0 + if backing: location(h.p_offset, h.p_filesz, length) + if h.p_type == "PT_LOAD" and h.p_memsz < h.p_filesz: + raise LayoutFailure("format", "ELF load segment memory size is smaller than its file size.") + segments.append({ + "index": index, "type": h.p_type, "offset": address(h.p_offset), + "header_location": location(image.header.e_phoff + index * image.header.e_phentsize, image.header.e_phentsize, length), + "file_size": address(h.p_filesz), "memory_size": address(h.p_memsz), + "virtual_address": address(h.p_vaddr), "physical_address": address(h.p_paddr), + "alignment": address(h.p_align), "flags": address(h.p_flags), + "file_backing": "file" if backing else "none", + "permissions": None if h.p_type == "PT_NULL" else {"read": bool(h.p_flags & 4), "write": bool(h.p_flags & 2), "execute": bool(h.p_flags & 1)}, + }) + if h.p_type == "PT_DYNAMIC": + tag_size = image.structs.Elf_Dyn.sizeof() + tags = [] + for tag_index in range(h.p_filesz // tag_size): + tag = segment._get_tag(tag_index) + tags.append(tag) + if tag.d_tag == "DT_NULL": + break + else: + raise LayoutFailure("format", f"ELF dynamic segment {index} has no complete DT_NULL terminator within its reported file range.") + for tag in tags: + if tag.d_tag == "DT_NEEDED": + needed.append(dynamic_name_reference(tags, tag.d_val, image, content)) + if h.p_type == "PT_INTERP": + raw = content[h.p_offset:h.p_offset + h.p_filesz] + end = raw.find(b"\0") + if end < 0: + raise LayoutFailure("format", "ELF interpreter lacks a reported terminator.") + interpreters.append({"display": raw[:end].decode("utf-8", "replace"), "bytes_base64": base64.b64encode(raw[:end]).decode("ascii"), "location": location(h.p_offset, end + 1, length), "unknown_reason": None}) + return { + "format": "elf", "architecture": {"machine": image.header.e_machine, "bits": image.bits, "byte_order": image.endian}, + "image_type": image.header.e_type, + "entry_point": {"reported_value": address(image.header.e_entry), "meaning": "not-applicable" if image.header.e_type == "ET_REL" else "absent" if image.header.e_entry == 0 else "linked-virtual-address", "execution_status": "unknown"}, + "runtime_load_base": None, "sections": sections, "segments": segments, "symbols": symbols, "relocations": relocations, + "packed_relative_relocations": packed_relatives, "relocation_inventory_completeness": "unknown", + "linkage": {"needed_libraries": needed, "interpreters": interpreters, + "got": [{"display_name": name, "address": address(value)} for name, value in image.got.items()], + "plt": [{"display_name": name, "address": address(value)} for name, value in image.plt.items()], + "convenience_maps_completeness": "unknown", "runtime_library_paths": None}, + "mitigations": {"evidence_kind": "inferred", "position_independent": image.pie, "nx_indicator": image.nx, "executable_stack_indicator": image.execstack, "stack_canary_indicator": image.canary, "relro": image.relro}, + "limitations": [ + "PT_NULL payload fields are unused and retain their reported numbers without file-byte or permission claims. Zero-file-size segments provide no file bytes.", + "SHN_XINDEX symbol values remain unresolved in this upstream representation; an external index table does not establish a resolved index in this report.", + "Reported values preserve linked addresses, section offsets, TLS offsets, alignment and absolute values separately. Runtime load base and library paths are unknown.", + "Names are display strings plus raw bytes and file ranges where resolvable; section/table/entry indices preserve identity and duplicates. Name ranges include their terminating NUL; raw name bytes exclude it.", + "Ordinary relocation rows cover REL/RELA. RELR tables retain complete encoded bytes and upstream-decoded offsets as derived evidence; per-offset packed-word locations and implicit addends remain unknown. Relocation inventory completeness is unknown, including other encodings and missing section tables.", + "Relocation symbol index zero uses a zero symbol value without a table lookup. Positive indices reference validated original symbol-table entries; raw table and symbol indices remain reported.", + "Relocatable relocation targets with SHN_UNDEF or inactive SHT_NULL headers retain their reported section index/offset as unknown; they do not establish an associated target section.", + "GOT/PLT are derived upstream convenience maps; PLT inference can emulate selected instructions in Unicorn. They do not establish target runtime behavior; aliases may collapse and warnings may indicate incomplete coverage. Their completeness is unknown.", + "Mitigations are upstream static heuristics, not runtime protection. ET_DYN does not prove an executable; absent canary symbols do not prove every function unprotected.", + "This profile inspects ELF EXEC/DYN/REL layout only. It does not analyze recorded cores, resolve loaded libraries, launch the target as a host process or start a debugger.", + "Symbol/relocation inventories reflect original section tables. A sectionless image can still report dynamic dependency names; missing tables do not prove absence of dynamic symbols or relocations.", + *(["Absent section-name table: section-dependent RELRO/canary heuristics may be incomplete; protection coverage is unknown."] if all_sections and section_name_index == 0 else []), + *(["Sectionless images lack the .dynamic section and symbol tables used by upstream heuristics. RELRO/canary indicators may be incomplete; their values remain reported static candidates, with protection coverage unknown."] if not all_sections else []), + ], + } + except LayoutFailure: + raise + except (ELFError, ValueError, AssertionError, IndexError) as error: + raise LayoutFailure("format", "Selected ELF failed unchanged upstream structural parsing: " + str(error)) from error + + + +if __name__ == "__main__": + runtime_path = Path(__file__).with_name("decoder_runtime.py") + runtime = {"__file__": str(runtime_path), "__name__": "rea_decoder_runtime"} + exec(compile(runtime_path.read_bytes(), str(runtime_path), "exec"), runtime) + runtime["main"](sys.argv[-1], inspect_elf, LayoutFailure, PROFILE) diff --git a/bridge/pwntools/recorded_crash.py b/bridge/pwntools/recorded_crash.py new file mode 100644 index 0000000000..f8bc3f3053 --- /dev/null +++ b/bridge/pwntools/recorded_crash.py @@ -0,0 +1,39 @@ +"""Catch typed resource failures while loading or running the owned recorded-core adapter. + +Only this package's fixed sibling is executed; caller input stays in request.json. +Interpreter startup failures before this boundary remain unclassified. +""" +import os +import sys + + +def resource_exit(status, marker): + try: + if len(sys.argv) > 2: + descriptor = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + os.write(descriptor, marker) + os.close(descriptor) + except (OSError, MemoryError): + # Without a complete marker the parent keeps this failure unclassified. + pass + os._exit(status) + +try: + implementation = os.path.join(os.path.dirname(__file__), "recorded_crash_impl.py") + with open(implementation, "rb") as handle: + source = handle.read() + exec(compile(source, implementation, "exec"), { + "__file__": implementation, + "__name__": "__main__", + }) +except MemoryError: + # Reserved status: an observed allocation failure, including imports/compile. + resource_exit(75, b"M") +except OSError as error: + # Linux ENOMEM is an observed allocation failure, independent of error text. + if error.errno == 12: + resource_exit(75, b"M") + # Linux EFBIG is observed directly, including a partial limits/reply write. + if error.errno == 27: + resource_exit(76, b"F") + raise diff --git a/bridge/pwntools/recorded_crash_impl.py b/bridge/pwntools/recorded_crash_impl.py new file mode 100644 index 0000000000..84aa72ac54 --- /dev/null +++ b/bridge/pwntools/recorded_crash_impl.py @@ -0,0 +1,143 @@ +"""Bounded Linux core evidence via unchanged ELFFile/structs and pwntools ctypes. + +No Corefile-selected thread, executable guessing, process lookup or target launch. +""" +import base64 +import ctypes +import importlib.metadata +import io +from pathlib import Path +import sys + +PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2" + + +class CoreFailure(Exception): + def __init__(self, reason, message): + super().__init__(message) + self.reason = reason + + +def encoded(value): + return base64.b64encode(value).decode("ascii") + + +def location(offset, size, limit): + if not 0 <= offset <= limit or not 0 <= size <= limit - offset: + raise CoreFailure("format", "Recorded source range exceeds its declared container.") + return {"offset": hex(offset), "bytes": hex(size)} + + +def inspect_core(path, cache): + for package, required in {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}.items(): + try: + actual = importlib.metadata.version(package) + except importlib.metadata.PackageNotFoundError as error: + raise CoreFailure("unavailable", "Selected Python lacks " + package + " " + required + ".") from error + if actual != required: + raise CoreFailure("unsupported", "Expected " + package + " " + required + "; selected Python reports " + actual + ".") + content = path.read_bytes() + if len(content) < 16 or content[:4] != b"\x7fELF": + raise CoreFailure("format", "Selected recording lacks a complete ELF identification.") + if content[4:6] != b"\x02\x01": + raise CoreFailure("unsupported-target", "Initial recorded-core profile requires ELF64 little-endian.") + if len(content) < 64: + raise CoreFailure("format", "Selected ELF64 core header is truncated.") + # Linux commonly emits ELFOSABI_NONE (0), also accepted as GNU/Linux (3). + # Other declared OSABIs must never select Linux note ctypes. + if content[7] not in (0, 3) or content[8] != 0: + raise CoreFailure("unsupported-target", "Initial recorded-core profile requires System V or GNU/Linux OSABI with ABI version zero; selected OSABI=" + str(content[7]) + ", ABI version=" + str(content[8]) + ".") + if int.from_bytes(content[16:18], "little") != 4 or int.from_bytes(content[18:20], "little") != 62: + raise CoreFailure("unsupported-target", "Initial recorded-core profile requires x86-64 ET_CORE.") + from elftools.common.exceptions import ELFError + from elftools.common.utils import struct_parse + from elftools.elf.elffile import ELFFile + from pwnlib.elf.datatypes import elf_prstatus_amd64, elf_siginfo_64, user_regs_struct_amd64 + try: + image = ELFFile(io.BytesIO(content)) + header = image.header + count = image.num_segments() + if count and header.e_phentsize < image.structs.Elf_Phdr.sizeof(): + raise CoreFailure("format", "Program-header entry is shorter than the required ELF64 structure.") + location(header.e_phoff, count * header.e_phentsize, len(content)) + segments, notes, padding, threads, signals = [], [], [], [], [] + for segment_index, segment in enumerate(image.iter_segments()): + h = segment.header + backing = h.p_type != "PT_NULL" and h.p_filesz > 0 + if backing: + location(h.p_offset, h.p_filesz, len(content)) + if h.p_type == "PT_LOAD" and h.p_filesz > h.p_memsz: + raise CoreFailure("format", "Recorded PT_LOAD file bytes exceed memory size.") + segments.append({"index": segment_index, "type": h.p_type, + "header_location": location(header.e_phoff + segment_index * header.e_phentsize, header.e_phentsize, len(content)), + "offset": hex(h.p_offset), "file_size": hex(h.p_filesz), "memory_size": hex(h.p_memsz), + "virtual_address": hex(h.p_vaddr), "physical_address": hex(h.p_paddr), "alignment": hex(h.p_align), "flags": hex(h.p_flags), "file_backing": "file" if backing else "none"}) + if h.p_type != "PT_NOTE" or h.p_filesz == 0: + continue + position, end = h.p_offset, h.p_offset + h.p_filesz + header_size = image.structs.Elf_Nhdr.sizeof() + while position < end: + if end - position < header_size: + raw = content[position:end] + if any(raw): + raise CoreFailure("format", "Nonzero trailing note bytes lack a complete note header.") + padding.append({"segment_index": segment_index, "location": location(position, len(raw), end), "bytes_base64": encoded(raw)}) + break + note = struct_parse(image.structs.Elf_Nhdr, image.stream, stream_pos=position) + owner_start = position + header_size + descriptor_start = owner_start + ((note.n_namesz + 3) // 4) * 4 + note_end = descriptor_start + ((note.n_descsz + 3) // 4) * 4 + location(position, note_end - position, end) + owner = content[owner_start:owner_start + note.n_namesz] + if owner and b"\0" not in owner: + raise CoreFailure("format", "Declared note owner has no NUL terminator.") + display = owner.split(b"\0", 1)[0].decode("utf-8", "replace") if owner else None + descriptor = content[descriptor_start:descriptor_start + note.n_descsz] + note_index = len(notes) + notes.append({"index": note_index, "segment_index": segment_index, "type": note.n_type, + "location": location(position, note_end - position, end), + "owner_location": location(owner_start, len(owner), end), "owner_bytes_base64": encoded(owner), "owner_display": display, + "descriptor_location": location(descriptor_start, len(descriptor), end), "descriptor_bytes_base64": encoded(descriptor)}) + position = note_end + # Only the initial Linux CORE ABI is interpreted. Other owners retain exact bytes. + if owner != b"CORE\0": + continue + if note.n_type == "NT_PRSTATUS": + if len(descriptor) < ctypes.sizeof(elf_prstatus_amd64): + raise CoreFailure("format", "Linux amd64 NT_PRSTATUS descriptor is truncated.") + status = elf_prstatus_amd64.from_buffer_copy(descriptor) + registers = [] + for name, kind in user_regs_struct_amd64._fields_: + relative = elf_prstatus_amd64.pr_reg.offset + getattr(user_regs_struct_amd64, name).offset + registers.append({"name": name, "value": hex(getattr(status.pr_reg, name)), "location": location(descriptor_start + relative, ctypes.sizeof(kind), end)}) + threads.append({"note_index": note_index, "historical_pid": ctypes.c_int32(status.pr_pid).value, "recorded_current_signal": status.pr_cursig, "registers": registers}) + elif note.n_type == "NT_SIGINFO": + if len(descriptor) < ctypes.sizeof(elf_siginfo_64): + raise CoreFailure("format", "Linux amd64 NT_SIGINFO descriptor is truncated.") + signal = elf_siginfo_64.from_buffer_copy(descriptor) + number = ctypes.c_int32(signal.si_signo).value + code = ctypes.c_int32(signal.si_code).value + known = number == 11 and code in (1, 2) + signals.append({"note_index": note_index, "number": number, "code": code, "errno": ctypes.c_int32(signal.si_errno).value, + "fault_address": hex(signal.sigfault_addr) if known else None, + "fault_address_meaning": "recorded-sigsegv-address" if known else "unknown", "thread_association": "unknown"}) + return {"format": "elf-core", "architecture": "x86_64-little-endian", "target_execution": "not-performed", "live_process_identity": "unknown", + "segments": segments, "notes": notes, "note_padding": padding, "threads": threads, "signals": signals, + "note_interpretation_completeness": "unknown", + "limitations": ["The initial Linux note profile accepts System V/unspecified or GNU/Linux OSABI with ABI version zero; an unspecified OSABI alone does not prove operating-system origin.", + "Historical PID and signal values are recorded metadata, not current process identity or attach authority.", + "Only exact Linux CORE NT_PRSTATUS/NT_SIGINFO owners are interpreted; every descriptor and owner retains original bytes. Other note semantics remain unknown.", + "NT_SIGINFO thread association is unknown. Only SIGSEGV SEGV_MAPERR/SEGV_ACCERR initially establish a fault-address union meaning.", + "PT_LOAD file bytes are recorded bytes. Undumped memory and current executable/library identity remain unknown; no current host file is resolved from a recorded pathname.", + "Missing notes/registers/signals do not establish their absence at the original crash. Full unwinding, executable pairing and live control are not provided."]} + except CoreFailure: + raise + except (ELFError, ValueError, AssertionError, IndexError) as error: + raise CoreFailure("format", "Recorded core failed unchanged upstream structural parsing: " + str(error)) from error + + +if __name__ == "__main__": + runtime_path = Path(__file__).with_name("decoder_runtime.py") + runtime = {"__file__": str(runtime_path), "__name__": "rea_decoder_runtime"} + exec(compile(runtime_path.read_bytes(), str(runtime_path), "exec"), runtime) + runtime["main"](sys.argv[-1], inspect_core, CoreFailure, PROFILE) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8d25bfc85d..3143620095 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -17,6 +17,7 @@ export default defineConfig({ text: "Getting started", items: [ { text: "Installation and setup", link: "/installation" }, + { text: "CLI and Evidence", link: "/cli" }, { text: "Agent prompts", link: "/mcp-prompts" }, { text: "Roadmap", link: "/roadmap" }, ], @@ -64,6 +65,7 @@ export default defineConfig({ { text: "MCP contracts", link: "/mcp-contracts" }, { text: "Tool design", link: "/tool-design" }, { text: "Testing", link: "/testing" }, + { text: "Releasing", link: "/releasing" }, { text: "Architecture decisions", link: "/adr/README" }, ], }, diff --git a/docs/apple-application-analysis.md b/docs/apple-application-analysis.md index bf606f34d7..e99504e164 100644 --- a/docs/apple-application-analysis.md +++ b/docs/apple-application-analysis.md @@ -110,3 +110,78 @@ The projection does not read plists. Use `inspect_plist` on a bundle's `info_plist_path` or a launchd plist to read `CFBundleExecutable`, identifiers, `SMPrivilegedExecutables`, `BundleProgram`, URL schemes, and usage descriptions. Use `inspect_signature` on an executable candidate for code-signing claims. + +## Dylib load resolution + +`trace_dylib_resolution` answers which file each Mach-O dylib load reaches. It +runs on the active Mach-O, or on every executable in the active `.app`. Each +executable in a bundle (the main app, XPC services, app extensions, login items, +privileged helpers, and helper tools) is a separate process root with its own +`@executable_path`. + +```sh +rea trace-dylib-resolution MyApp.app --json +rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture arm64 --json +``` + +```json +{ + "name": "trace_dylib_resolution", + "arguments": { "roots": ["Contents/MacOS/MyApp"], "architecture": "arm64" } +} +``` + +Load commands are parsed in TypeScript from Mach-O and FAT headers, including +the `dylib_use_command` encoding newer linkers emit for flags such as delayed +initialization. Nothing is executed and no Apple tool is required, so the tool +also works off macOS. + +Resolution follows dyld: + +- `@executable_path` is the directory of the process root's executable. It is + undetermined when the root is a library. +- `@loader_path` is the directory of the image that holds the load command, or + of the image that owns the `LC_RPATH` entry being expanded. +- `@rpath` tries each `LC_RPATH` of the loading image first, then those of the + image that loaded it, up to the process root. +- Dependencies load dependents-first in load-command order, and each image + loads once per process. A later request whose install name matches an + already loaded image reuses it. +- Symlinks such as `Versions/Current` are followed inside the analyzed root. + An escape through `..` or an absolute link is `escapes-target`. + +Each edge lists every candidate path tried, with its outcome: `resolved`, +`absent`, `not-mach-o`, `malformed`, `unsupported` (recognized Mach-O outside +parser coverage, such as big-endian images), `architecture-missing`, +`outside-target`, `escapes-target`, or `undetermined`. Slices follow dyld's +graded architectures, so an `x86_64h` process also loads `x86_64`. The edge's `resolution` is one of: + +| Status | Meaning | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. | +| `conditional` | An image inside the root resolves, but an earlier candidate outside the root (for example `/usr/lib/swift`) would win if it exists on the running system. | +| `unresolved` | Every candidate is definitively absent or unusable inside the root. | +| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. | + +A `conditional` image is still traced, because it is the image the analyzed +root supplies. Every edge below it has `loader_conditional: true`, and a +request that reuses it by install name is `conditional` too. Findings on those +edges say that they apply only if the conditional image loads. + +Absolute install names such as `/usr/lib/libSystem.B.dylib` are +`outside-target`. On macOS 11 and later most of them live in the dyld shared +cache rather than on disk, so REA does not check them against the host. + +`findings` are derived, not observed: + +- `required-load-unresolved`, `weak-load-unresolved`, and `lazy-load-unresolved`. + `LC_LAZY_LOAD_DYLIB` dependencies are resolved but not traversed, because dyld + loads them on first use; an unresolved one is not a launch failure. +- `earlier-rpath-candidate-absent`: a Mach-O placed at an earlier search path + would load first. Whether code-signing library validation would reject it is + not evaluated; use `inspect_signature`. +- `dyld-environment-present`: `LC_DYLD_ENVIRONMENT` search paths are not modeled. + +`verify:macos-bundle` checks the parser against `otool -l` for every traced +image. It also compares the predicted load order of two process roots with the +images dyld actually loads (`DYLD_PRINT_LIBRARIES`). diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index f4835fdb2e..b02bfafbe9 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -26,11 +26,13 @@ graph TD end subgraph "Provider Layers" + PWNTOOLS["src/native/pwntools/ + src/native/pwndbg/
BYO offline ELF/core adapters
owned snapshots/Python and optional core-only GDB
unchanged upstream parsers, ABI structures and mapping API"] + EVM["src/evm/
unchanged EVMole/WASM in bounded owned worker
explicit carrier parsing and inferred interface adapter"] HOPPER["src/hopper/
Hopper launch & Unix-socket protocol
BridgeLauncher, HopperClient, protocol"] GHIDRA["src/ghidra/
Ghidra discovery, profile & function analysis
snapshot, Unix/Windows transport, bounded lifecycle"] IDA["src/ida/
Upstream MCP contract adaptation
live GUI binding or owned headless database"] IDA_MCP["External ida-pro-mcp
GUI proxy or database supervisor
upstream installation and engine lifecycle"] - AUX["src/artifacts/ + src/native/
disjoint artifact and native capabilities"] + AUX["src/artifacts/ + src/native/
artifact inventory and extraction
format readers and native capabilities"] BROWSER["src/browser/
CDP / Playwright capture
captured scripts and native module resolution
execution/: coverage, listener and source attribution"] JS_CODEC["src/javascript/sourceMaps/
pinned upstream map codec
owned heap-limited Node process, complete point evidence"] INSPECTOR["src/inspector/
passive V8 Inspector discovery and capture
script and target locations"] @@ -92,6 +94,9 @@ graph TD APP -->|record callbacks| RECORDS SERVER -->|narrow record ports| RECORDS RECORDS --> DOMAIN + COMPOSITION --> PWNTOOLS + PWNTOOLS --> PROCESS + PWNTOOLS --> DOMAIN COMPOSITION --> HOPPER COMPOSITION --> GHIDRA COMPOSITION --> IDA @@ -140,3 +145,8 @@ graph TD TESTS -.-> BROWSER TESTS -.-> EVALUATION TESTS -.-> ERRORS + + COMPOSITION --> EVM + APP -->|EVM interface port| EVM + EVM --> PROCESS + EVM --> DOMAIN diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md new file mode 100644 index 0000000000..e337e0fa1d --- /dev/null +++ b/docs/binary-diagnostics.md @@ -0,0 +1,131 @@ +# Offline binary diagnostics + +`inspect_binary_layout` / `inspect-binary-layout` answers what an explicit +file contains: section/segment layout, original symbols/relocations, static +linkage names and mitigation indicators. It works without an active Hopper, +Ghidra or IDA target. CTF is one possible use; crash triage, compatibility and +ordinary binary inspection use the same modular capability. + +## Bring your own engine + +Initial real verification covers **Linux x64, ELF64 x86-64 little-endian +EXEC/DYN/REL**. Provide an absolute Python executable whose environment already +contains unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2: + +```sh +REA_PWNTOOLS_PYTHON=/absolute/isolated-env/bin/python \ + rea inspect-binary-layout ./selected.elf --json +``` + +```json +{ + "name": "inspect_binary_layout", + "arguments": { "path": "/artifacts/selected.elf" } +} +``` + +REA never installs Python, packages or GDB, changes a user init file, launches +the selected object as a host process, or requests runtime library resolution. The Python process +uses isolated mode and an owned cache. Exact upstream profiles are recorded in +[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/pwntools/README.md). + +## Interpreting results + +- Artifact path, SHA-256 and size identify original selected bytes. All parsing + reads an owned stable snapshot; the original is unchanged. +- Addresses, offsets, lengths and flags are hexadecimal strings. A linked + address is not a runtime address. Runtime load base remains null. A zero + EXEC/DYN entry means absent; a relocatable entry is not applicable. +- Each symbol retains its table and entry index. Its reported value can mean + undefined, alignment, absolute value, no address, unknown section index, + section offset, TLS offset or linked virtual address. Duplicate names remain + separate entries. SHN_XINDEX remains unresolved in the selected upstream + representation; its external index table is not silently treated as resolved. +- Relocatable objects have section-relative relocations and no executable entry + claim. Signed REL/RELA relocation addends are decimal strings. Packed RELR + tables retain full original bytes and upstream-decoded offsets as derived + evidence, with per-offset packed-word locations and implicit addends unknown. + Overall relocation inventory completeness remains unknown. + A relocatable SHN_UNDEF target or inactive SHT_NULL header remains an explicit + unknown with the original reported section index and offset. +- REL/RELA symbol references retain their original table and entry indices. + Positive indices resolve through a validated symbol table; symbol index zero + means a zero symbol value without a table lookup, including when no table is + linked. Malformed references fail with the affected section and symbol index. +- Section-name and symbol tables require declared SHT_STRTAB links. + SHN_UNDEF explicitly means no section names: display is empty, raw name and + location unknown, with the original name offsets retained. Extended section-name + indices resolve through section zero and undergo the same validation. +- Symbol tables require a declared SHT_STRTAB link. Name offsets and terminators + must stay inside the declared string table; malformed references fail before + unrelated bytes can become names. Dynamic dependencies use DT_STRTAB/DT_STRSZ + with a unique file-backed mapping; ambiguous or unbacked mappings are + explicitly unsupported. +- Name display strings use UTF-8 replacement for opaque bytes, including + sectionless dependency names and interpreter paths. Raw name + bytes and string-table ranges retain observed identity where resolvable; + ranges include the terminating NUL and base64 bytes exclude it. +- NOBITS and NULL sections provide no file bytes. Reported file-backed ranges + are checked against snapshot size again at the public boundary. PT_NULL payload + fields are unused: reported numbers remain, file backing is none and interpreted + permissions are null. Segments with zero file size also have no file bytes. +- Sectionless dynamic images still report dependency names with raw bytes and + file locations where uniquely mapped. Missing symbol/relocation section tables + do not establish that those runtime tables are absent. + Upstream RELRO/canary heuristics rely on sections and can be incomplete in + this case; their reported candidates retain an explicit coverage limitation. +- DT_NEEDED/PT_INTERP are reported names, not resolved runtime paths. GOT/PLT + maps are derived convenience views and may collapse aliases. PLT inference + can emulate selected instructions in Unicorn; its outputs are static candidates. + Exec syscall tracing observes host process launches. Map completeness + remains unknown and original upstream warnings are returned inline. +- Canary, PIE, NX, executable-stack and RELRO are static inferences. NX and + executable-stack are separate upstream indicators: on x86-64 an executable + stack can accompany an unknown (`null`) NX indicator. ET_DYN can be a shared + library, and an absent canary symbol does not prove every function unprotected. + +Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics +and 30-second owned command deadline. The reply budget applies to the decoder +record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The +pinned MCP SDK defaults to a 10 MiB receive buffer. Large complete results need +a caller-configured `StdioClientTransport({ maxBufferSize: ... })`; the large +extended-index verification uses 256 MiB and an explicit five-minute request +timeout in its separate opt-in MCP lane. REA does not silently truncate the result. Python lowers resource soft limits to at most 3 GiB virtual address space, +30 CPU seconds and 64 MiB file output. Inherited tighter soft/hard limits are +retained, and the effective values are returned in the result limitations. Virtual address space is not RSS; +Unicorn needs a large virtual map. Limits fail with no partial success, and +owned process/root cleanup completes independently of cancellation. + +Core files, GDB session/state/control, optional pwndbg enrichment and instruction +inspection are separate increments tracked by #969. This increment makes no +real support claim for those features or other operating systems/architectures. + +Typed decoder failures retain bounded `captured_output` (stdout, stderr and an +explicit truncation flag) alongside the original failure category and reason. +Successful `diagnostics` also exposes the supervisor's `truncated` flag. Output +beyond the complete diagnostic budget is rejected, including when the decoder +writes a valid reply; cleanup and late cancellation reuse the observed flag. +Memory allocation failures use `resource_constraint`, with effective resource +limits (or an explicit unknown) and memory-specific recovery guidance. A +MemoryError alone does not establish the exact failed allocation or that a +particular budget was exhausted. Invalid undersized symbol entries are rejected +before REA reports source ranges for them. +The bridge reserves 1 MiB to report allocation failures. If even error reporting +or serialization fails with MemoryError, its reserved exit status preserves the +resource classification with unknown effective limits. +Reserved exits require a matching private marker written by the bridge's actual +failure branch. Bare launcher exits or missing/unwritable markers retain process +diagnostics with the resource cause unverified. +Signal termination alone is never classified as observed memory exhaustion. +The small bootstrap also covers catchable MemoryError during adapter imports, +compilation and initialization. Python interpreter startup failures before the +bootstrap runs retain their observed process diagnostics without guessed causes. +SIGXCPU retains a CPU resource diagnostic and CPU-specific recovery advice. +Observed EFBIG writes or SIGXFSZ termination retain a file-size resource +diagnostic and file-size-specific recovery advice. If a tight file limit also +prevents writing the limit record, effective values remain unknown. +The bridge records actual limits in owned storage before analysis; missing or +malformed limit reports remain unknown with their read failure preserved. A +received signal alone does not establish its exact cause. Dynamic tags require +a complete DT_NULL inside PT_DYNAMIC; interpreter names and ranges end at the +first NUL, with any padding still represented by the original segment range. diff --git a/docs/browser-observation.md b/docs/browser-observation.md index cea8f79a63..3be1b37f19 100644 --- a/docs/browser-observation.md +++ b/docs/browser-observation.md @@ -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. diff --git a/docs/capability-migration.md b/docs/capability-migration.md index 2ce6471a4b..1a757dd829 100644 --- a/docs/capability-migration.md +++ b/docs/capability-migration.md @@ -385,6 +385,30 @@ claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecy claims require the real Apple lane. Native binary API/value-flow semantics and shared cross-domain contract aggregates keep their separate owners. +## Artifact inventory and extraction ownership + +`src/artifacts/inventory/` owns format classification, reader selection, inventory +policy, canonical scanning and artifact graph construction. The shared artifact +provider and JavaScript reconstruction workflow consume this owner. Snapshot +semantics remain in `src/domain/artifactInventorySnapshot.ts`. + +`src/artifacts/extraction/` owns member selection, integrity checks and transactional +materialization through the existing readers and safe output tree. Reader cleanup, +cancellation, rollback, archive containment and caller-visible results retain their +existing behavior. Acquisition guards reject imports from these producer owners +into application workflows, production composition or CLI/MCP adapters. + +`src/application/artifacts/ArtifactExtractionDestination.ts` owns the caller's +default temporary output-root choice for both CLI and MCP. The adapters pass this +destination to the existing provider operation; format handling and member writes +remain in the artifact owner. + +Add format-specific acquisition behavior beside the inventory readers and exercise +its actual producer representation in filesystem boundary tests. Add caller +workflow behavior in the application owner and verify both adapters. The installed +package lane checks inventory identity and extraction through CLI and MCP; the +real Apple bundle lane separately establishes DMG mount and detach behavior. + ## Binary production composition closure `src/composition/binary.ts` is the shared production owner for deep candidate @@ -425,3 +449,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. diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000000..ae7da86607 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,184 @@ +# CLI and Evidence + +Use the CLI for a direct inspection or a script. It uses the same application +workflows and evidence contracts as REA's MCP server. Each CLI invocation is a +separate process; save results when you want to use them in a later command. + +## Run a command + +After [installing REA](installation.md), use `rea`. You can also run a command +without a global installation: + +```bash +npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json +``` + +Use an extracted application directory or an ASAR as the target. Static +JavaScript analysis returns the application graph and Evidence directly. +Generic `rea analyze PATH` selects this workflow for directories and `.asar` +files when neither `--provider` nor `--snapshot` is supplied. See +[JavaScript artifact reconstruction](javascript-artifact-reconstruction.md) +for results, integrity checks and coverage. + +## Native analysis + +Configure [Hopper or Ghidra](installation.md#hopper), or the +[IDA adapter](ida-provider.md), before analyzing a native target. Substitute +your target, search text and function name or address in these examples: + +```bash +rea analyze /absolute/path/to/program --provider ghidra --json +rea search /absolute/path/to/program "search" --provider ghidra --json +rea function /absolute/path/to/program main --provider ghidra --json +rea decompile /absolute/path/to/program 0x1000 --provider ghidra --json +rea xrefs /absolute/path/to/program 0x1000 --provider ghidra --json +rea trace /absolute/path/to/program "search" --provider ghidra --json +``` + +`analyze` and `inspect` share the native overview workflow. `function` returns +a function dossier; `decompile` returns pseudocode. For assembly instructions +alone, use `rea instructions`. A macOS `.app` bundle can be supplied directly. + +Run `rea --help` or a command's `--help` for arguments and output options. +The [generated catalog](product-catalog.json) lists all CLI commands and MCP +tools. Provider-specific target support is described in the +[native guide](native-investigation.md), [DOS guide](ghidra-dos.md), and +[Windows Ghidra guide](windows-ghidra-p0.md). + +## Choose a provider + +List the available providers and their supported operations: + +```bash +rea providers --json +rea capabilities --json +``` + +These commands describe binary-session providers and auxiliary capabilities. +For the complete MCP surface, use the connected server's tool list and the +session's advertised availability; individual guides describe prerequisites. + +With automatic selection, REA uses the single available provider that supports +the target. When several providers support it, select one explicitly: + +```bash +rea analyze /absolute/path/to/program --provider hopper +``` + +Set `REA_ANALYSIS_PROVIDER` for a standing preference. An explicit `--provider` +overrides it. In MCP, pass `provider_id` to `open_binary`: + +```json +{ + "path": "/absolute/path/to/program", + "provider_id": "hopper" +} +``` + +The selected provider remains bound to the session until an explicit switch +or close. Provider failures are returned with their original reason. For an +`ambiguous` selection error, choose from `details.candidate_ids`; for +`provider_unavailable`, run `rea doctor --provider ID --json` to diagnose the +selected engine. See [task readiness](installation.md#check-readiness-for-your-task) +and [provider selection](adr/0001-provider-selection-and-analysis-profiles.md). + +The session reports work still in progress through `analysis_activity`. +A client timeout can end its wait while the provider continues analyzing. +`cleanup_incomplete` identifies resources whose shutdown or removal could not +be verified. See [MCP contracts](mcp-contracts.md) for session lifecycle and +[Ghidra first-query deadlines](mcp-contracts.md#ghidra-first-query-deadlines-and-recovery) +for import, client timeouts and recovery. + +## Save and reuse analysis snapshots + +A snapshot retains successful analysis results for later queries. REA reuses +an exact result when the target bytes, operation, parameters, provider and +settings match. Mutations and cursor-dependent calls are excluded from the +cache. Snapshot files are local and use owner-only permissions. + +```bash +rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program.json +# Repeat the same query to reuse its saved result. +rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program.json +``` + +An exact CLI cache hit is read before starting a provider process. +In MCP, `open_binary` accepts `snapshot_path` to import a snapshot atomically +for its matching target; an MCP provider may still start before returning a +cached result. `close_binary` accepts `snapshot_path` and optional +`overwrite: true` to save before releasing provider resources. A failed save +leaves the session open so the caller can resolve the output failure. + +## Import, export and compare Evidence + +Evidence records retain artifact identity, source locations, observations, +inferences and unresolved findings. Validate an existing bundle, export its +canonical form, or compare two supplied bundles: + +```bash +rea evidence-import /absolute/path/to/evidence/bundle.json +rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json +rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json +``` + +Exports preserve an existing destination unless `--overwrite` is explicit. +For application traces and version comparisons, see +[JavaScript application workflows](javascript-application-workflows.md). +MCP [retained Evidence references](mcp-contracts.md#retained-application-evidence-inputs) +belong to one connection. Separate CLI calls consume full saved records. + +## Import historical source + +Import an older source tree to compare with the current artifact: + +```bash +rea import-reference-source /absolute/path/to/source +``` + +The import records hashes and metadata for the supplied files, separately +from observations of the current app. File names do not automatically exclude +files. Set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON array of ignore +patterns when you want to exclude selected paths. + +Historical-source import requires safe no-follow file opens on Linux or +macOS. Native Windows returns `unsupported_host`; use Linux REA inside WSL +or another supported host. See +[source-to-bundle comparison](javascript-application-workflows.md#historical-source-to-bundle-comparison) +for mapping identities, inferences and unknowns. + +## Capture runtime behavior + +Choose the guide for the target and intended interaction: + +- [Browser observation](browser-observation.md): inspect a selected page through CDP. +- [Browser scenarios](browser-scenario-contract.md): run declared interactions and capture their results. +- [Electron observation](electron-observation.md): inspect an Electron renderer or capture an application scenario. +- [Node/Electron Inspector](javascript-runtime-observation.md): record script locations and execution contexts. +- [Process capture](process-capture.md): run an executable and compare terminal, exit and filesystem observations. + +Runtime requests name the target and actions. Launched targets run with your +user permissions; consult the chosen guide for host requirements and effects. + +## Output and exit status + +The default terminal format is TOON. Use `--json` when saving results for a +JSON consumer. Output selection and formatting do not change operation status. + +| Status | Meaning | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `0` | The operation completed. Its result may include partial evidence, warnings or unresolved questions. | +| `1` | The operation could not complete. Structured output identifies invalid input, permissions, cancellation, timeouts or another failure when available. | +| `128 + N` | Signal `N` ended the process, where the shell or runtime preserves the conventional signal-derived status. | + +`setup --dry-run` returns `planned` and exits `0`; a cancelled setup also exits +`0`. Setup returns `1` for `needs_confirmation` or `needs_human`. +`doctor` returns `1` when required checks in its selected readiness scope fail; +unavailable optional providers remain informational for unrelated tasks. + +Enable `pipefail` in a supporting shell so a downstream formatter preserves +REA's failure status: + +```bash +set -o pipefail +rea inspect-artifact ./app.asar --json | jq . > inspection.json +``` diff --git a/docs/error-contract.schema.json b/docs/error-contract.schema.json index 5f276b5512..b79e8cee31 100644 --- a/docs/error-contract.schema.json +++ b/docs/error-contract.schema.json @@ -10,10 +10,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -29,7 +31,79 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} + } + }, + "required": ["code", "category", "message", "retryable", "remediation"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "code": { "type": "string", "const": "access_denied" }, + "category": { + "type": "string", + "enum": [ + "invalid_input", + "unsupported_provider", + "unsupported_target", + "integrity_mismatch", + "truncated", + "cancelled", + "timeout", + "resource_constraint", + "unavailable", + "execution_failure" + ] + }, + "message": { "type": "string", "minLength": 1 }, + "retryable": { "type": "boolean" }, + "remediation": { + "type": "object", + "properties": { "action": { "type": "string", "minLength": 1 } }, + "required": ["action"], + "additionalProperties": false + }, + "details": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} + } + }, + "required": ["code", "category", "message", "retryable", "remediation"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "code": { "type": "string", "const": "artifact_changed" }, + "category": { + "type": "string", + "enum": [ + "invalid_input", + "unsupported_provider", + "unsupported_target", + "integrity_mismatch", + "truncated", + "cancelled", + "timeout", + "resource_constraint", + "unavailable", + "execution_failure" + ] + }, + "message": { "type": "string", "minLength": 1 }, + "retryable": { "type": "boolean" }, + "remediation": { + "type": "object", + "properties": { "action": { "type": "string", "minLength": 1 } }, + "required": ["action"], + "additionalProperties": false + }, + "details": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -44,10 +118,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -63,7 +139,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -78,10 +154,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -97,7 +175,43 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} + } + }, + "required": ["code", "category", "message", "retryable", "remediation"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "code": { "type": "string", "const": "unsupported_target" }, + "category": { + "type": "string", + "enum": [ + "invalid_input", + "unsupported_provider", + "unsupported_target", + "integrity_mismatch", + "truncated", + "cancelled", + "timeout", + "resource_constraint", + "unavailable", + "execution_failure" + ] + }, + "message": { "type": "string", "minLength": 1 }, + "retryable": { "type": "boolean" }, + "remediation": { + "type": "object", + "properties": { "action": { "type": "string", "minLength": 1 } }, + "required": ["action"], + "additionalProperties": false + }, + "details": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -112,10 +226,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -131,7 +247,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -146,10 +262,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -165,7 +283,43 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} + } + }, + "required": ["code", "category", "message", "retryable", "remediation"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "code": { "type": "string", "const": "resource_constraint" }, + "category": { + "type": "string", + "enum": [ + "invalid_input", + "unsupported_provider", + "unsupported_target", + "integrity_mismatch", + "truncated", + "cancelled", + "timeout", + "resource_constraint", + "unavailable", + "execution_failure" + ] + }, + "message": { "type": "string", "minLength": 1 }, + "retryable": { "type": "boolean" }, + "remediation": { + "type": "object", + "properties": { "action": { "type": "string", "minLength": 1 } }, + "required": ["action"], + "additionalProperties": false + }, + "details": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -180,10 +334,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -199,7 +355,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -214,10 +370,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -270,10 +428,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -289,7 +449,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -304,10 +464,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -323,7 +485,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -338,10 +500,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -357,7 +521,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -372,10 +536,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -391,7 +557,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -406,10 +572,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -425,7 +593,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -440,10 +608,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -459,7 +629,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -474,10 +644,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -493,7 +665,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -508,10 +680,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -527,7 +701,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -542,10 +716,12 @@ "enum": [ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure" ] @@ -561,27 +737,11 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], "additionalProperties": false } - ], - "$defs": { - "__schema0": { - "anyOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "type": "null" }, - { "type": "array", "items": { "$ref": "#/$defs/__schema0" } }, - { - "type": "object", - "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } - } - ] - } - } + ] } diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md new file mode 100644 index 0000000000..8db25973ed --- /dev/null +++ b/docs/evm-bytecode.md @@ -0,0 +1,84 @@ +# Offline EVM bytecode + +`inspect_evm_interface` / `inspect-evm-interface` infers interface candidates +from one explicitly selected local bytecode carrier. The capability is general: +contract investigation, compatibility, audit and CTF showcases use the same +portable primitive. It does not require an active native binary target. + +```sh +rea inspect-evm-interface ./runtime.hex hex --json +rea inspect-evm-interface ./runtime.bin raw --json +``` + +```json +{ + "name": "inspect_evm_interface", + "arguments": { "path": "/artifacts/runtime.hex", "encoding": "hex" } +} +``` + +Initial real verification covers flat legacy bytecode on Linux x64 and unchanged +bundled EVMole 0.9.3. EOF-style EF00 containers return `unsupported_target`, +with the selected path and format reason. Provider/configuration failures retain +their distinct unavailable-provider or capability classification. +The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`; +`REA_EVM_PRLIMIT_COMMAND` can select another absolute executable. REA installs +no system tools and changes no user configuration. [Upstream provenance and +license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) are packaged. + +## Evidence semantics + +- `encoding` is required: raw bytes or UTF-8 hex, with optional `0x` prefix and + outer ASCII whitespace. Odd hex, interior separators, BOM, non-ASCII whitespace + and invalid UTF-8 are rejected. Empty bytecode is valid and has unknown coverage. +- Carrier SHA-256 identifies the exact original file, including prefix/casing + and whitespace. Decoded-byte SHA-256 identifies actual selected EVM bytes. + Neither digest is the Ethereum Keccak code hash. +- Full bytecode, upstream function offsets and original producer representation + are returned inline. Offsets are byte positions in decoded bytecode. +- Upstream also returns terminal metadata during selector recovery. Its reported + values remain in `raw_result`; integer precision and non-text-key coverage are + unknown in the JS binding. Full bytecode retains the original encoded metadata. +- Selectors, argument strings and state mutability are **inferences**. Function + names/signatures are not looked up. Optimized code may produce different + mutability candidates; recovered types are not a verified source ABI. +- Runtime/init-code identity, deployed authenticity, hardfork and discovery + completeness remain unknown. Empty candidates do not prove no callable interface. +- Analysis is offline: no target runtime execution, implicit chain/RPC/explorer + lookup, wallet action, transaction broadcast or network request. + +## Resource and lifecycle limits + +Inputs are limited to 4 MiB, complete replies to 16 MiB and combined diagnostic +output to 1 MiB. An owned worker has a 256 MiB JavaScript heap, 3 GiB virtual +address-space limit, 30 CPU seconds and a 30-second command deadline. Virtual +address space differs from resident memory. The worker retains inherited tighter +soft/hard limits, reports its configured soft limits inline and leaves hard limits +unchanged. WASM trap reservations are disabled +so the engine can run within that virtual limit; actual memory is not equated to +its 3 GiB allowance. Limits fail without partial success. Stable snapshots, +private files and owned processes are cleaned independently of caller cancellation. +Invalid-input, unsupported, cancellation, timeout and malformed-reply failures +retain bounded stdout/stderr under `details.captured_output`, including whether +diagnostic output was truncated. Cleanup failures preserve the +original error projection and any captured output. +Successful results expose `diagnostics.truncated` from the supervisor as well. +Excess diagnostic output fails the complete-output contract even when the worker +has written a valid reply. +SIGXCPU retains a CPU resource diagnostic and CPU-specific guidance. Observed +file-size write failures (EFBIG) and SIGXFSZ likewise retain a file-size resource +diagnostic. Exact signal/write causes remain unknown. Configured +soft limits are reported separately from unknown effective limits; the received +signal alone does not establish its exact cause. +Reserved file-size exits require a matching private marker written by the +worker's EFBIG branch. Bare launcher exits or unwritable markers retain process +diagnostics with the resource cause unverified. + +Source/metadata, proxy resolution, storage/CFG and fork-explicit execution/traces +remain separate increments under #972, with materially different result or +execution authority. This primitive makes no deployment or runtime behavior claim. + +The real verification lane traces representative raw/hex CLI workflows and checks +that only declared Node/prlimit launchers and process ownership inspection run. +Socket traces permit local stdio socket pairs and reject attempted network requests. +These observations verify those fixtures; they are not a general network sandbox. diff --git a/docs/installation.md b/docs/installation.md index 9caa72c656..bc21b585b1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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 @@ -90,7 +95,7 @@ npx skills add morluto/rea --skill reverse-engineer-anything This installs agent instructions and bundled references, not REA MCP registration or analysis engines. Follow the skill's -[conditional connection guide](https://github.com/morluto/rea/blob/main/skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed). +[conditional connection guide](https://github.com/morluto/rea/blob/main/skill-src/reverse-engineer-anything/SKILL.md#connect-only-when-needed). Working tools can be used immediately. If tools are missing, inspect the current client's registration with `doctor --client codex --json` (substitute its client ID), then plan repairs with `setup --client codex --dry-run --json`. Show and @@ -110,7 +115,7 @@ npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app -- It returns the complete Evidence record directly and requires no native engine. Provider failures in doctor do not prevent unrelated target-free tools. To check readiness for one task instead of auditing every integration, see -[Check readiness for the task at hand](https://github.com/morluto/rea/blob/main/README.md#check-readiness-for-the-task-at-hand). +[Check readiness for your task](#check-readiness-for-your-task). ## Supported agents @@ -213,12 +218,44 @@ installed REA skill. Run the returned scoped setup command to review and approve those changes, then restart affected agents. The plan is returned in terminal, non-TTY, and JSON modes without applying configuration changes. +## Check readiness for your task + +Run `rea doctor` when you need diagnosis. It reads host prerequisites and agent +configuration without changing them. Without options, it audits every detected +registration, the installed skill and optional analysis engines; its overall +`healthy` value can be false while your chosen workflow works. + +Select the part you want to check: + +| Task | Readiness check | +| ----------------------------------- | ------------------------------------------------------------------------ | +| Static JavaScript/Electron analysis | Run `rea analyze-javascript-application PATH --json` directly. | +| One analysis engine | `rea doctor --provider ghidra --json` (or `hopper`, `ida`) | +| One agent registration | `rea doctor --client codex --json` (see [client IDs](#supported-agents)) | +| Installed workflow instructions | `rea doctor --skill --json` | + +A scoped report has `scope.mode: "explicit"`. Its `scope_checks` determine +`healthy` and the exit status. Other checks appear in `informational_checks`; +`environment_healthy` summarizes the full audit. `--target` adds a target check, +but the report remains audit-wide unless a scope option is also supplied. + +Follow the remediation for the failed check. If a native target has several +available providers, choose one with `--provider` on the CLI or `provider_id` +on `open_binary`. A `capability_unavailable` failure carries +`details.selection_reason`: `ambiguous` asks you to choose from +`details.candidate_ids`, while `provider_unavailable` asks you to repair the +selected engine. See [provider selection](cli.md#choose-a-provider). + ## Hopper Hopper is separate commercial software with its own license. Its free demo has vendor-defined limits, and a paid license is optional. REA reuses any detected installation and preserves Hopper during uninstall. +The supported native host baseline is macOS 12+, Ubuntu 24.04+, Fedora 41+, +64-bit Arch Linux, or CachyOS. Ghidra and IDA have their own provider-specific +host requirements; Windows Ghidra uses the [experimental P0 boundary](windows-ghidra-p0.md). + On macOS, approved setup downloads the official DMG, checks its published size and digest, validates the application bundle, and atomically installs it to `~/Applications/Hopper Disassembler.app`. REA then opens Hopper so the @@ -247,6 +284,40 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes `sudo`. `rea doctor --provider hopper --json` reports the selected strategy and both host and effective mount facts. +### Launcher paths and troubleshooting + +On macOS, REA uses +`/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` by default. +On Linux, it prefers executable `/opt/hopper/bin/Hopper`, then executable +`~/.local/share/rea/hopper/bin/Hopper`; if neither exists, the former remains +the diagnostic fallback. `HOPPER_LAUNCHER_PATH` overrides those choices: + +```bash +export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper +rea doctor --provider hopper --json +``` + +If a Linux launcher exists but cannot start, inspect its shared libraries: + +```bash +ldd /absolute/path/to/Hopper | grep 'not found' +``` + +Install the missing packages and rerun the scoped check. Linux demo sessions +need Xvfb, Python 3, X11 and XTEST; approved setup installs those dependencies +on supported distributions. Add the curl installer's reported executable +directory, commonly `~/.local/bin`, to your shell `PATH` if needed. + +REA starts Hopper when needed. On macOS, its launcher can bring a window or +first-run dialog forward; choose demo mode or activate your existing license. +Hopper serializes analysis requests. Cancelling a wait can leave provider work +running, which the session reports. Successful decompilation is cached until +a relevant rename or comment changes it. + +Closing a session shuts down REA's bridge and removes its temporary socket +directory while preserving the Hopper application. A `cleanup_incomplete` +result identifies resources whose cleanup could not be verified. + ### Hopper in CI REA's unattended Linux path is validated against Hopper's offered demo mode. @@ -289,7 +360,7 @@ entry comments atomically and returns refreshed analysis. These session metadata edits leave executable bytes unchanged and are discarded on close. GUI controls require Hopper; Windows P0 remains read-only. -Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes. +Windows P0 admits native x86 and x86-64 PE applications on fixed local NTFS volumes. The npm package bundles native Job Object ownership, protected private runtime DACLs, and handle-based path admission; no separate addon installation is needed. See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and @@ -389,6 +460,17 @@ Ghidra/JDK installation; neither is inferred from startup alone. `rea doctor --json` is strictly read-only. `rea update` updates only the npm installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths. +```bash +rea update +rea uninstall +rea uninstall --purge-data +``` + +Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills +and other MCP servers. Purging removes only REA's cache and state under +`~/.rea`; malformed client configuration or purge-path symlinks stop the +operation. See the [CLI guide](cli.md#output-and-exit-status) for exit statuses. + ## MCP Registry REA is published in the official MCP Registry as `io.github.morluto/rea`. Registry @@ -408,7 +490,7 @@ For a client that requires manual configuration, use: "mcpServers": { "rea": { "command": "npx", - "args": ["-y", "rea-agents@4.1.0", "mcp"] + "args": ["-y", "rea-agents@6.0.0", "mcp"] } } } diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 97d6cd41a8..f3eda9698c 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -31,6 +31,11 @@ command and the generic route return their complete results inline. Native targets, single JavaScript files, `.app` bundles, and explicit deep-provider or snapshot requests retain the native deep-analysis route. +Both CLI routes emit `rea_progress` JSON lines on stderr as inventory, source +parsing, graph construction, validation, and Evidence hashing begin. Stdout +remains the selected result document. Completion is reported after Evidence +creation; a failed analysis retains its typed diagnostic result. + Configure an MCP client with the ordinary REA setup command: ```bash @@ -52,19 +57,57 @@ manifest and graph commitments, JavaScript Application Graph, static Electron summary, reconstruction statistics, and explicit limitations. It does not require a live Hopper, Ghidra, browser, or Electron process. +## ASAR integrity + +ASAR inventory checks Electron integrity metadata for embedded archive entries +and supplied `.asar.unpacked` companion files. An integrity failure identifies +the logical path, declared and calculated SHA-256 values, and whether the entry +was unpacked. By default, a mismatch is returned as a failure with its artifact +context. Requests that support `integrity_policy` can explicitly select +`record-and-continue` to inspect verified siblings while retaining the mismatch. + +An unpacked entry whose companion bytes were not supplied remains +`unavailable`. REA continues analyzing embedded JavaScript and records the +missing native/resource bytes as unknown. See [what is reconstructed](#what-is-reconstructed) +for the inventory fields and [MCP integrity handling](mcp-contracts.md#integrity-record-and-continue) +for the tool-result contract. + ## Large results +Application analysis projects semantic relationships one source file at a time. +After projection, it retains only the lexical module facts and exported return +shapes needed to build application relationships. Full source IR does not stay +resident for every file together. Callable ownership and contained-reference +queries use per-file range indexes, while call sites use exact-range lookups. +Existing node budgets and coverage reporting remain in effect. + +Progress identifies the source file being parsed and projected. Cancellation is +checked between files, with event-loop yields that allow notifications and +completed file-local allocations to be released. A single file's synchronous +parser and semantic extraction still need to finish before that boundary. + Graph and Evidence identifiers hash canonical JSON incrementally, without assembling a single string for the whole graph. The canonical bytes and existing identifiers remain unchanged. The opt-in regression check is `npm run verify:javascript:digests`; it hashes a value larger than the running Node engine's single-string limit. -Output formatters and MCP transport serialization still assemble whole strings. -For a large CLI result, select the needed fields -before formatting, for example `--format json --filter-output -evidence_id,normalized_result.statistics`. Complete serialization of a result -beyond the engine's string limit is not established by this digest check. +Both application CLI routes stream complete JSON and JSONL output in bounded +chunks, including field filters and `--full-output`. They wait for each stdout +write instead of constructing or reparsing a document-sized string. The opt-in +`npm run verify:javascript:output` checks the actual CLI formatting boundary with +a temporary output file larger than the running engine's string limit, verifies +its bytes and digest, and removes it. Use `-- jsonl` for the compact JSONL check. +Each check needs space for one output file plus a 1 GiB free-space reserve. + +Other CLI formats, `--token-count`, and MCP transport serialization still +assemble whole strings. Field selection remains useful when the caller needs a +smaller view, for example `--format json --filter-output +evidence_id,normalized_result.statistics`. Streaming output does not bound the +memory needed to construct the analysis graph itself. +The separate large-response transport work is tracked in +[#1053](https://github.com/morluto/rea/issues/1053) and +[#1050](https://github.com/morluto/rea/issues/1050). Client framing limits also apply: the pinned Node MCP SDK's stdio transport defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate the complete response, including text and structured Evidence projections. @@ -193,3 +236,27 @@ paired, ambiguous, and unpaired IPC, validation candidates, utility processes, and native binding requests. These fixtures establish parser and artifact-reader claims; they do not replace the later operator-supplied real-application benchmark. + +URI schemes are classified independently of local file names: a reference such +as `web3:app.js` remains external even when an artifact has that literal name. +Scheme characters may include digits after the initial letter; a relative path +such as `./web3:app.js` still names a local artifact. + +Package `exports` fallback arrays are supported both at the top level and under +the root `"."` entry. The resolver selects targets in declared order using the +same conditional and invalid-entry handling as nested exports arrays. + +ESM relative module paths and selected package exports targets use URL suffix handling +and one percent-decoding pass. CommonJS relative paths and legacy package main +fields retain literal filename punctuation. Exports targets must start with +`./` and exclude `.`, `..`, and `node_modules` path segments, including their +encoded forms, before URL normalization. Arrays skip invalid exports targets +in declared order; URL decoding and file lookup failures for a selected target +do not select a later entry. Rejected references retain the package metadata +path, original target, failed constraint, and importing source location. + +HTML script references resolve to exact inventoried files after applying the +document base and query/fragment rules. CommonJS module lookups retain extension +and directory resolution. +Unresolved HTML references retain their declaration, source range, and resolution +reason in the renderer observations. diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 336cf0c535..b0a0df4c04 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -25,7 +25,7 @@ part of the current tool set. This guide describes the implementation and verification of [ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md). The canonical -tool inventory is [`product-catalog.json`](https://github.com/morluto/rea/blob/main/docs/product-catalog.json). +tool inventory is the [build-generated catalog](mcp-contracts.md#generated-catalog). ## Shipped scope @@ -125,7 +125,7 @@ The upstream analyzer currently gates its own analysis to x86-64. NativeAOT's broader platform matrix does not establish REA coverage: PE/COFF, ELF, Mach-O, shared libraries, target architectures, and runtime metadata versions each need their own provider verification. REA's experimental Windows Ghidra boundary is -limited to admitted native x86-64 PE applications, so it does not currently +limited to admitted native x86 and x86-64 PE applications, so it does not currently establish coverage for PE DLLs. Mobile NativeAOT targets should remain experimental until REA has a matching provider and package-level verification. diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 224dc79c78..ce99456d2c 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -1,5 +1,15 @@ # MCP runtime contracts +## Generated catalog + +Run `npm run build:cached` in a source checkout to generate the machine-readable +catalog at `docs/public/product-catalog.json`. The [catalog](product-catalog.json) +is also included in documentation builds. PR CI retains it with the packaged +skill and portable conformance projections in the `generated-docs` artifact. +These outputs describe the exact source revision being built; they are not +checked-in snapshots. For a running server, `binary_session` remains the +authoritative source of catalog identity and tool availability. + ## Identity and discovery `binary_session` reports the active package, server, SDK, and negotiated @@ -13,6 +23,24 @@ are currently unavailable. Opening or closing a target or observing a provider health transition leaves that catalog unchanged and does not emit `notifications/tools/list_changed`. +Advertised input and output schemas contain no reachable recursive references. +The input compatibility profile limits nesting to ten object, array, and +`anyOf`/`oneOf`/`allOf` levels, following local references per path. Property +maps, reference definitions, and example data do not add schema levels. Tests +check the complete catalog after SDK conversion and its generated counterpart. +This is REA's local compatibility profile; individual model APIs can impose +additional limits. + +For passive `compare_web_captures` inputs, pass each complete +`inspect_web_page` result in `before.inspection` or `after.inspection`, with an +optional complete `discover_webmcp_tools` result in the matching `webmcp` field. +The input schema describes these producer-result objects as round-trip payloads. +For scenario comparisons, pass complete `capture_browser_scenario` results in +`before_scenario` and `after_scenario`. REA validates all nested fields with the +original capture schemas; observation output schemas remain complete. Scenario +authoring inputs and comparison normalization options retain their full +advertised structure. + Call `binary_session` with `{}` and read `result.tool_availability` to choose a callable operation for the current target, provider, host, and negotiated client capabilities. The default result includes the complete inventory with each @@ -241,3 +269,6 @@ Verified siblings continue. Extraction writes the observed bytes and returns the contradictions with its result; JavaScript analysis reads contradicted files as observed and lists them in `integrity_contradictions`. Comparisons classify the result as a contradiction and reconstruction cannot treat it as unchanged. +Application comparisons and feature traces retain the source integrity warnings +and report incomplete coverage. Extraction retains nested contradiction records +without claiming that the nested members were materialized separately. diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 48bd8e55b8..48860c5311 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -12,6 +12,13 @@ input file and include the observed lipo slice offset for universal binaries. If that offset is unavailable, segment evidence locations are omitted with an explicit limitation; architecture inventory locations remain available. +- `trace_dylib_resolution` / `rea trace-dylib-resolution ` + parses Mach-O load commands in TypeScript and follows dyld's path expansion + for every executable in an app bundle, or for one Mach-O within its directory. + It reports each `@rpath`, `@loader_path` and `@executable_path` candidate with + its outcome. Paths outside the analyzed root, including shared-cache system + libraries, stay undetermined. See + [Apple application analysis](apple-application-analysis.md#dylib-load-resolution). - `inspect_asset_catalog` / `rea inspect-asset-catalog ` reads compiled `Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw rendition fields, pagination and exact UI resource-name matches are returned. @@ -159,6 +166,47 @@ owned helper per scenario, removes its temporary compiler cache and stops its helper on cancellation. It does not launch or own the selected application. UI actions may change application data or trigger network activity. +## Native call observation + +`observe_native_calls` / `rea observe-native-calls ` +launches the active Mach-O as a new, owned process under LLDB. It stops at +caller-selected entries, records them and continues: + +- functions by exact symbol name, optionally limited to one image; +- Objective-C methods by selector, optionally limited to one class and to + instance or class methods. + +```sh +rea observe-native-calls /Applications/Example.app '{"breakpoints":[{"kind":"objc-method","class_name":"NSURLSession","selector":"dataTaskWithRequest:completionHandler:"},{"kind":"function","name":"open","module":"libsystem_kernel.dylib"}],"duration_ms":5000,"backtrace_frames":4}' --json +``` + +Each event is an observation: thread, image, symbol, load and file address, +the raw integer argument registers, optional caller frames and, for an +Objective-C method, the selector read from `_cmd` and the receiver's dynamic +class. Breakpoints stop at the symbol itself, so the registers hold the ABI +arguments. The receiver class comes from LLDB's Objective-C runtime reader +without running target code; no expression is evaluated, and REA never +attaches to a process it did not launch. + +The run ends when the process exits, `max_events` entries are recorded, or +`duration_ms` elapses. In the last two cases the process is killed, and REA +confirms that it is gone. The result reports: + +- the outcome, exit status and captured stdout/stderr (1 MiB kept per stream); +- signal or exception stops; +- every breakpoint's resolved locations; a breakpoint that matched no loaded + code makes coverage partial; +- limitations. + +Only entries are observed: return values, floating-point and stack arguments, +and inlined or `objc_direct` calls are not. The target runs with the current +user's permissions and may change files, show UI or use the network. + +Debugging must be allowed for the user (Developer Tools access). A +hardened-runtime target without `com.apple.security.get-task-allow` cannot be +debugged; `inspect_signature` reports this as the debugger-attach facet, and +the tool fails with a `debugger-attach-denied` reason. + ## Provider and verification boundaries Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to diff --git a/docs/process-capture.md b/docs/process-capture.md index 8ed6302059..78d84bf910 100644 --- a/docs/process-capture.md +++ b/docs/process-capture.md @@ -30,6 +30,14 @@ different recovery: reinstall REA for the active platform, architecture, and Node.js version with optional dependencies enabled. Capability diagnostics distinguish this from the Windows capture-adapter limitation. +macOS also requires Apple's Swift compiler through `xcrun` for process +ownership inspection. REA compiles its packaged, narrow process-inspection +helper into a private temporary directory before a capture or owned provider +process starts; the same prerequisite applies to owned provider-process +supervision on macOS. REA removes that directory when it exits. REA does not +install Xcode, Command Line Tools, or other software. Capability checks prepare +this helper before reporting macOS capture as available. + ## Capture a command Write a JSON scenario and pass its path to the CLI: @@ -94,6 +102,12 @@ run the same owned-process cleanup path. Settlement reports whether the sampled process group quiesced or whether cleanup was needed or unverifiable; sampling cannot prove that every short-lived or detached descendant was seen. +When the host withholds an unrelated process’s ownership token, REA leaves that +process untouched and records its PID and reason in `cleanup.unverified_processes` +and process residual unknowns. Successful cleanup verifies the owned group; +it does not attribute those unrelated processes. Related or otherwise unexplained +unreadable processes still prevent successful cleanup. + ## Compare two captures Compare saved capture Evidence with: diff --git a/docs/product-catalog.json b/docs/product-catalog.json deleted file mode 100644 index 354884682a..0000000000 --- a/docs/product-catalog.json +++ /dev/null @@ -1,684 +0,0 @@ -{ - "package": { - "name": "rea-agents", - "version": "4.1.0", - "sdk": { - "server": "2.3.1", - "client": "2.3.1", - "core": "2.3.1" - }, - "skill_version": "27" - }, - "tools": { - "total": 133, - "families": [ - { - "id": "direct", - "surface": "official-proxy", - "count": 41, - "tools": [ - "address_name", - "address_to_file_offset", - "annotate_native_function", - "comment", - "current_address", - "current_document", - "current_procedure", - "goto_address", - "inline_comment", - "inspect_native_data_type", - "inspect_native_instruction", - "inspect_native_load_image", - "list_bookmarks", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "next_address", - "prev_address", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "procedure_references", - "read_bytes", - "read_function_instructions", - "resolve_containing_procedure", - "resolve_native_call_targets", - "search_procedures", - "search_strings", - "set_address_name", - "set_addresses_names", - "set_bookmark", - "set_comment", - "set_current_document", - "set_inline_comment", - "unset_bookmark", - "xrefs" - ] - }, - { - "id": "enhanced", - "surface": "enhanced", - "count": 14, - "tools": [ - "analyze_function", - "analyze_swift_types", - "batch_decompile", - "binary_overview", - "find_xrefs_to_name", - "get_call_graph", - "get_objc_classes", - "get_objc_protocols", - "inspect_native_api", - "inspect_native_dispatch_metadata", - "trace_call_path", - "trace_feature", - "trace_native_ui_action", - "trace_native_values" - ] - }, - { - "id": "native", - "surface": "native-provider", - "count": 7, - "tools": [ - "capture_native_ui_scenario", - "demangle_swift", - "inspect_macho", - "inspect_plist", - "inspect_signature", - "list_architectures", - "observe_native_ui" - ] - }, - { - "id": "artifact", - "surface": "artifact-provider", - "count": 5, - "tools": [ - "decode_interface_builder", - "extract_artifact", - "inspect_artifact", - "inspect_asset_catalog", - "inspect_keyed_archive" - ] - }, - { - "id": "managed", - "surface": "managed", - "count": 7, - "tools": [ - "compare_managed_members", - "import_managed_reconstruction", - "inspect_managed_artifact", - "inspect_managed_members", - "inspect_managed_native_boundaries", - "project_managed_application_graph", - "verify_managed_native_boundaries" - ] - }, - { - "id": "firmware", - "surface": "firmware-provider", - "count": 2, - "tools": ["extract_firmware", "inspect_firmware_regions"] - }, - { - "id": "android", - "surface": "android-provider", - "count": 5, - "tools": [ - "inspect_android_class", - "inspect_android_method", - "inspect_android_package", - "search_android_classes", - "trace_android_references" - ] - }, - { - "id": "browser", - "surface": "browser-provider", - "count": 11, - "tools": [ - "analyze_web_bundle", - "capture_browser_scenario", - "capture_web_screenshot", - "compare_web_captures", - "compare_web_screenshots", - "discover_webmcp_tools", - "inspect_web_event_listeners", - "inspect_web_page", - "list_browser_targets", - "observe_web_execution", - "observe_web_session" - ] - }, - { - "id": "electron", - "surface": "electron-provider", - "count": 5, - "tools": [ - "analyze_javascript_application", - "capture_electron_scenario", - "inspect_electron_page", - "list_electron_targets", - "reconcile_javascript_runtime" - ] - }, - { - "id": "javascript-runtime", - "surface": "runtime-provider", - "count": 2, - "tools": [ - "list_javascript_runtime_targets", - "observe_javascript_runtime" - ] - }, - { - "id": "application", - "surface": "application-workflow", - "count": 13, - "tools": [ - "build_reconstruction_obligation_ledger", - "compare_application_versions", - "compare_javascript_export_shapes", - "compare_source_to_bundle", - "evaluate_reconstruction_coverage", - "export_web_scripts", - "project_android_application_graph", - "project_apple_application_graph", - "recover_javascript_sources", - "trace_application_feature", - "trace_javascript_semantics", - "trace_web_module_imports", - "trace_web_source_location" - ] - }, - { - "id": "session", - "surface": "session", - "count": 21, - "tools": [ - "binary_session", - "build_call_path", - "capture_process_scenario", - "close_binary", - "compare_artifacts", - "compare_bundles", - "compare_functions", - "compare_process_captures", - "correlate_static_and_runtime", - "export_evidence_bundle", - "find_changed_behavior", - "get_evidence_bundle", - "get_navigation_context", - "import_evidence_bundle", - "inspect_address_context", - "list_unknowns", - "open_binary", - "record_unknown", - "update_unknown", - "verify_reconstruction", - "verify_unknown_resolution" - ] - } - ] - }, - "providers": [ - { - "id": "binwalk", - "name": "Binwalk", - "version": "3.1.0", - "capabilities": ["inspect_firmware_regions"] - }, - { - "id": "ghidra", - "name": "Ghidra", - "version": null, - "capabilities": [ - "address_name", - "address_to_file_offset", - "analyze_function", - "annotate_native_function", - "inspect_native_data_type", - "inspect_native_instruction", - "inspect_native_load_image", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "procedure_references", - "read_bytes", - "read_function_instructions", - "resolve_containing_procedure", - "resolve_native_call_targets", - "search_procedures", - "search_strings", - "xrefs" - ] - }, - { - "id": "hopper", - "name": "Hopper Disassembler", - "version": null, - "capabilities": [ - "address_name", - "address_to_file_offset", - "analyze_function", - "comment", - "current_address", - "current_document", - "current_procedure", - "goto_address", - "inline_comment", - "list_bookmarks", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "next_address", - "prev_address", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "procedure_references", - "read_bytes", - "read_function_instructions", - "resolve_containing_procedure", - "search_procedures", - "search_strings", - "set_address_name", - "set_addresses_names", - "set_bookmark", - "set_comment", - "set_current_document", - "set_inline_comment", - "unset_bookmark", - "xrefs" - ] - }, - { - "id": "ida", - "name": "IDA Pro MCP adapter", - "version": "1", - "capabilities": [ - "analyze_function", - "list_procedures", - "list_strings", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_pseudo_code", - "read_function_instructions", - "search_procedures", - "search_strings", - "xrefs" - ] - }, - { - "id": "jadx", - "name": "JADX headless", - "version": "0.7.1", - "capabilities": [ - "inspect_android_class", - "inspect_android_method", - "inspect_android_package", - "search_android_classes", - "trace_android_references" - ] - }, - { - "id": "native-macos", - "name": "macOS native inspection utilities", - "version": null, - "capabilities": [ - "capture_native_ui_scenario", - "demangle_swift", - "inspect_macho", - "inspect_plist", - "inspect_signature", - "list_architectures", - "observe_native_ui" - ] - }, - { - "id": "rea-android-application", - "name": "REA Android application workflows", - "version": "1", - "capabilities": ["project_android_application_graph"] - }, - { - "id": "rea-apple-application", - "name": "REA Apple application workflows", - "version": "1", - "capabilities": ["project_apple_application_graph"] - }, - { - "id": "rea-artifact-graph", - "name": "REA safe artifact graph provider", - "version": "1", - "capabilities": [ - "decode_interface_builder", - "extract_artifact", - "inspect_artifact", - "inspect_asset_catalog", - "inspect_keyed_archive" - ] - }, - { - "id": "rea-cdp-browser", - "name": "REA Chrome DevTools Protocol observation provider", - "version": "2", - "capabilities": [ - "analyze_web_bundle", - "capture_web_screenshot", - "compare_web_captures", - "compare_web_screenshots", - "discover_webmcp_tools", - "inspect_web_event_listeners", - "inspect_web_page", - "list_browser_targets", - "observe_web_execution", - "observe_web_session" - ] - }, - { - "id": "rea-cdp-electron", - "name": "REA Electron file-page CDP observation provider", - "version": "1", - "capabilities": ["inspect_electron_page", "list_electron_targets"] - }, - { - "id": "rea-dotnet-static", - "name": "REA managed static analysis provider", - "version": "1", - "capabilities": [ - "inspect_managed_artifact", - "inspect_managed_members", - "inspect_managed_native_boundaries" - ] - }, - { - "id": "rea-dotnet-workflows", - "name": "REA managed-code workflows", - "version": "1", - "capabilities": [ - "compare_managed_members", - "import_managed_reconstruction", - "project_managed_application_graph", - "verify_managed_native_boundaries" - ] - }, - { - "id": "rea-javascript-application", - "name": "REA JavaScript application analyzer", - "version": "1", - "capabilities": ["analyze_javascript_application"] - }, - { - "id": "rea-javascript-application-workflows", - "name": "REA JavaScript application workflows", - "version": "1", - "capabilities": [ - "build_reconstruction_obligation_ledger", - "compare_application_versions", - "compare_javascript_export_shapes", - "compare_source_to_bundle", - "evaluate_reconstruction_coverage", - "trace_application_feature", - "trace_javascript_semantics" - ] - }, - { - "id": "rea-javascript-runtime-reconciliation", - "name": "REA JavaScript runtime reconciliation", - "version": "1", - "capabilities": ["reconcile_javascript_runtime"] - }, - { - "id": "rea-playwright-browser-scenario", - "name": "REA Playwright browser scenario capture provider", - "version": "1", - "capabilities": ["capture_browser_scenario"] - }, - { - "id": "rea-playwright-electron-active", - "name": "REA Playwright active Electron observation provider", - "version": "1", - "capabilities": ["capture_electron_scenario"] - }, - { - "id": "rea-v8-inspector", - "name": "REA passive Node/Electron V8 Inspector provider", - "version": "1", - "capabilities": [ - "list_javascript_runtime_targets", - "observe_javascript_runtime" - ] - }, - { - "id": "rea-web-script-export", - "name": "REA captured script export", - "version": "1", - "capabilities": [ - "export_web_scripts", - "trace_web_module_imports", - "trace_web_source_location" - ] - }, - { - "id": "unblob", - "name": "Unblob", - "version": "26.6.4", - "capabilities": ["extract_firmware"] - }, - { - "id": "wakaru", - "name": "Wakaru", - "version": "1.13.0", - "capabilities": ["recover_javascript_sources"] - } - ], - "setup_clients": [ - { - "id": "claude_code", - "display_name": "Claude Code", - "format": "json", - "configuration": "managed" - }, - { - "id": "claude_desktop", - "display_name": "Claude Desktop", - "format": "json", - "configuration": "managed" - }, - { - "id": "codex", - "display_name": "Codex", - "format": "toml", - "configuration": "managed" - }, - { - "id": "cursor", - "display_name": "Cursor", - "format": "json", - "configuration": "managed" - }, - { - "id": "gemini_cli", - "display_name": "Gemini CLI", - "format": "json", - "configuration": "managed" - }, - { - "id": "windsurf", - "display_name": "Windsurf", - "format": "json", - "configuration": "managed" - }, - { - "id": "devin", - "display_name": "Devin", - "format": "json", - "configuration": "managed" - }, - { - "id": "opencode", - "display_name": "OpenCode", - "format": "opencode", - "configuration": "managed" - }, - { - "id": "antigravity", - "display_name": "Antigravity", - "format": "json", - "configuration": "managed" - }, - { - "id": "copilot_cli", - "display_name": "GitHub Copilot CLI", - "format": "copilot_cli", - "configuration": "managed" - }, - { - "id": "commandcode", - "display_name": "Command Code", - "format": "commandcode", - "configuration": "managed" - }, - { - "id": "vscode", - "display_name": "VS Code", - "format": "vscode", - "configuration": "managed" - } - ], - "cli": { - "primary_count": 88, - "commands": [ - "address-to-file-offset", - "analyze", - "analyze-javascript-application", - "analyze-web-bundle", - "annotate-native-function", - "build-reconstruction-obligation-ledger", - "capabilities", - "capture-browser-scenario", - "capture-electron-scenario", - "capture-native-ui-scenario", - "capture-process", - "capture-web-screenshot", - "compare", - "compare-application-versions", - "compare-javascript-export-shapes", - "compare-managed-members", - "compare-process-captures", - "compare-source-to-bundle", - "compare-web-captures", - "compare-web-screenshots", - "decode-interface-builder", - "decompile", - "demangle-swift", - "discover-webmcp-tools", - "doctor", - "evaluate-reconstruction-coverage", - "evidence-export", - "evidence-import", - "export-web-scripts", - "extract-artifact", - "extract-firmware", - "function", - "import-managed-reconstruction", - "import-reference-source", - "inspect", - "inspect-android-class", - "inspect-android-method", - "inspect-android-package", - "inspect-artifact", - "inspect-asset-catalog", - "inspect-electron-page", - "inspect-firmware-regions", - "inspect-keyed-archive", - "inspect-macho", - "inspect-managed-artifact", - "inspect-managed-members", - "inspect-managed-native-boundaries", - "inspect-native-api", - "inspect-native-data-type", - "inspect-native-dispatch-metadata", - "inspect-native-instruction", - "inspect-native-load-image", - "inspect-plist", - "inspect-signature", - "inspect-web-event-listeners", - "inspect-web-page", - "instructions", - "list-architectures", - "list-browser-targets", - "list-electron-targets", - "list-javascript-runtime-targets", - "observe-javascript-runtime", - "observe-native-ui", - "observe-web-execution", - "observe-web-session", - "project-android-application-graph", - "project-apple-application-graph", - "project-managed-application-graph", - "providers", - "read-bytes", - "reconcile-javascript-runtime", - "recover-javascript-sources", - "resolve-native-call-targets", - "search", - "search-android-classes", - "setup", - "trace", - "trace-android-references", - "trace-application-feature", - "trace-javascript-semantics", - "trace-native-ui-action", - "trace-native-values", - "trace-web-module-imports", - "trace-web-source-location", - "uninstall", - "update", - "verify-managed-native-boundaries", - "xrefs" - ], - "aliases": [ - { - "name": "compare-bundles", - "target": "compare" - } - ] - }, - "runtime_catalog": { - "counts": { - "cli_commands": 88, - "mcp_tools": 133, - "mcp_prompts": 6 - }, - "digests": { - "tools_sha256": "4e652b6689a76b0d4c9c64f1ed819b9fdf2bfc995df23be8cfb4fa1334b4224f", - "prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d", - "combined_sha256": "63c52e101898418ed77fed2c9c4fc83f743f945581e35637683ffdd282919525", - "providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896" - } - } -} diff --git a/docs/provider-evaluation.md b/docs/provider-evaluation.md index 69a6214195..baf5740852 100644 --- a/docs/provider-evaluation.md +++ b/docs/provider-evaluation.md @@ -18,7 +18,7 @@ comments atomically, verifies readback, and returns refreshed function analysis. Changes live only in the session database and leave executable bytes unchanged. Ghidra does not expose GUI controls; Windows P0 remains read-only. -Windows x64 P0 supports native x86-64 PE applications on local NTFS with +Windows x64 P0 supports native x86 and x86-64 PE applications on local NTFS with bundled Job Object ownership, protected runtime DACLs, and handle-based path admission. Real ordinary-user verification covers all 25 read-only operations through the packaged CLI and MCP, target integrity, and cleanup. This remains @@ -49,8 +49,8 @@ follow. - Report GUI cursor/navigation and persistent mutation operations as unavailable until their semantics and project ownership are explicitly designed. - Verify real claims on Linux with at least two distinct source-owned binaries. - Verify the Windows host claim with the deterministic source-owned x86-64 PE - fixture and all admitted operations; compare normalized semantic facts rather + Verify the Windows host claim with the deterministic source-owned x86 and x86-64 PE + fixtures and all admitted operations; compare normalized semantic facts rather than provider-specific pseudocode text. ## Shipped foundation boundary @@ -62,7 +62,7 @@ maximum, when the release leaves those at the 12.1 defaults). Otherwise doctor probes `java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission and target compatibility are separate checks. Windows x64 P0 admits only native, -non-managed, non-DLL x86-64 PE applications on fixed local NTFS, with the packaged +non-managed, non-DLL x86 and x86-64 PE applications on fixed local NTFS, with the packaged native authority available before launch. The launcher creates one ephemeral runtime root with project, @@ -136,7 +136,7 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set these tools before compilation. Keeping this matrix separate lets Linux host/provider acceptance run with only the host compiler. -`npm run verify:ghidra:windows` checks the source-owned native x86-64 PE +`npm run verify:ghidra:windows` and its `-- --x86` variant check the source-owned native x86 and x86-64 PE fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage, and project, endpoint, process, and runtime cleanup. The independent native lane checks DACLs, handle admission, cancellation, and Job Object lifecycle. @@ -165,12 +165,12 @@ only the generic process mechanisms. ## Shortlist -| Provider | License / automation surface | What it brings | REA fit and blockers | -| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. | -| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. | -| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. | -| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. | +| Provider | License / automation surface | What it brings | REA fit and blockers | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86 and x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. | +| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. | +| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. | +| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. | ## Recommended order diff --git a/docs/recorded-crashes.md b/docs/recorded-crashes.md new file mode 100644 index 0000000000..d68db74fd8 --- /dev/null +++ b/docs/recorded-crashes.md @@ -0,0 +1,88 @@ +# Recorded crash evidence + +`inspect_recorded_crash` / `rea inspect-recorded-crash` inspect a caller-selected +Linux ELF64 x86-64 little-endian core independently of an active binary target. + +```sh +export REA_PWNTOOLS_PYTHON=/absolute/existing/venv/bin/python +rea inspect-recorded-crash ./crash.core --json +``` + +The selected Python needs unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. +REA does not install these dependencies. ELFFile/its original ELF structures and +pwntools' original Linux amd64 ctypes provide parsing and ABI layout. The adapter +adds bounds, representation preservation and lifecycle ownership. +The initial Linux note profile accepts System V/unspecified and GNU/Linux OSABI +with ABI version zero. Other declared ABIs return `unsupported_target` before +note interpretation; an unspecified OSABI alone does not prove OS origin. + +## Recorded observations + +- Artifact path, original SHA-256 and size; full normalized result and Evidence + are inline through CLI and MCP. +- Original program headers and recorded segment file/memory ranges. Undumped + memory and current executable/library identity remain unknown. +- Every note's original identity, owner/descriptor bytes and source ranges, + including unfamiliar owners and note kinds. +- Every exact Linux CORE PRSTATUS note as a separate recorded thread, with + historical PID, signal and lossless uint64 registers bound to source bytes. + Each supported note has exactly one interpretation; each thread contains the + complete 27-register ABI set, with names bound to their original ABI offsets. + Physical note ranges cannot overlap or leave segment gaps. ELF and note headers + are independently bound to the snapshot, including extended program counts. +- Signed SIGINFO number/code/errno. Initially only SIGSEGV codes 1/2 establish the + fault-address union; other variants retain raw bytes and unknown meaning. + +Historical PIDs are metadata. They never select a live process or authorize +attach/control. Missing notes do not establish absence at the crash; note +interpretation completeness and signal-thread association remain unknown. + +## Optional debugger mapping context + +```sh +export REA_PWNDBG_GDB=/absolute/existing/gdb +export REA_PWNDBG_GDBINIT=/absolute/unchanged/pwndbg/gdbinit.py +export REA_PWNDBG_VENV_PATH=/absolute/existing/pwndbg-venv +rea inspect-recorded-crash ./crash.core --debugger-context --json +``` + +MCP uses `{ "path": "/artifacts/crash.core", "include_debugger_context": true }`. +Omitting this meaningful facet requests only recorded evidence. Requested but +unavailable context returns an actionable failure; it never silently claims +debugger coverage. GDB defaults to `/usr/bin/gdb`; the plugin and virtual +environment require explicit absolute paths. Initial local verification covers +Linux x64, GDB 13.1 and unchanged pwndbg 2026.09.15. + +The outer provider owns both processes and one stable core snapshot. An owned +Python launcher lowers inherited soft limits before replacing itself with GDB. +GDB starts without initfiles, auto-load, debuginfod or automatic shared-library +loading. Owned empty library search paths and caches are used, with plugin +updates disabled. Fixed package code verifies a core connection and no paired +executable before querying mapping candidates. No caller GDB command language is +exposed. The exact observed GDB/plugin version strings are retained. + +Mapping names are producer display metadata, with unknown current file identity. +Map permissions are derived from reported flags; zero/unfamiliar flags leave +permissions unknown. These maps do not establish page contents or an executable +match. Core source bytes remain available independently of this enrichment. +An Evidence envelope containing these maps is `derived`; inspection without +debugger context is `observed`. + +## Bounds and verification + +Initial Linux x64 limits: 32 MiB input, 64 MiB decoder reply, 1 MiB diagnostics per +stage, 30 seconds owned wall deadline per stage, 3 GiB address-space soft ceiling +and 30 seconds CPU soft ceiling. Tighter inherited soft/hard limits are retained; +hard limits are not raised. Complete oversized results fail explicitly. These +process limits do not constitute a sandbox for caller-supplied engine code. + +Resource exit statuses need matching private failure-branch markers. Unmarked +launcher failures retain their actual status/output and unknown resource cause. +Cancellation, output truncation and independent owned cleanup remain explicit. + +`npm run verify:recorded:crash` uses source-owned fixtures and caller-supplied +GDB/Python/pwndbg/strace. Fixture generation explicitly launches its owned test +program; subsequent inspection only reads the resulting core. Real acceptance +checks recorded threads, high registers, missing/malformed notes, opaque owners, +CLI/MCP parity, optional debugger context and historical PID collision with an +owned live sentinel. Original inputs and host configuration remain unchanged. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000000..56fc662641 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,185 @@ +# Releasing from a checkpoint + +REA releases use an explicit source checkpoint. Main can continue accepting +changes while a release is tested and published. The Release workflow runs +only when a maintainer dispatches it; main pushes do not refresh release PRs. + +## 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 +that commit. The version is a maintainer choice; Release Please's proposed +version must agree before publication. + +`release/VERSION` also sets the expected version for the workflow. Use an exact +SemVer such as `release/6.1.0` or `release/6.1.0-rc.1`, without build metadata. +The checkpoint validator rejects a candidate whose package, lockfile, manifest, +registry metadata or changelog disagrees with that version before creating a tag. + +### Compatibility and version selection + +REA uses the default SemVer strategy: compatible fixes increment patch; +compatible capabilities increment minor; incompatible public behavior increments +major. Adding tools, internal refactoring and correcting behavior outside the +supported contract do not by themselves require a major release. + +The supported public surface includes documented CLI commands/options, MCP tool +names and input/result contracts, saved evidence formats and supported runtime +requirements. Before adding `!` or a `BREAKING CHANGE` footer, a PR must identify +a previously valid call or configuration that will fail or change meaning, +explain why compatibility cannot be preserved, and give its migration. Prefer +optional additions, compatibility adapters and a documented deprecation period; +group deliberate removals into a planned major release. Maintainers review this +impact before merging; the bot parses markers and cannot infer compatibility. + +For example, adding an optional inspection tool is a minor change. Rejecting a +relative MCP path previously accepted by the public contract is breaking; +rejecting an input that the existing contract already prohibited is a fix. +Changing the version number alone does not restore compatibility. + +For example, after selecting a reviewed commit for 5.0.0: + +```bash +git fetch origin main +git branch release/5.0.0 SOURCE_SHA +git push origin release/5.0.0 +``` + +Replace `SOURCE_SHA` with the selected full commit SHA. The checkpoint must +include this release workflow and CI support for release branches. For an +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. + +## 2. Prepare the bot PR + +Run the workflow definition from main and select the frozen source separately: + +```bash +gh workflow run release.yml --ref main \ + -f release_branch=release/5.0.0 -f phase=prepare +``` + +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). + +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. + +The validator reads the actual ancestry range from the published baseline tag +to the selected checkpoint, including visible first-parent Conventional Commits +and the PR titles in GitHub's default merge messages. The report preserves the +original merge subject alongside the extracted Conventional Commit title. +This matters after merging a side-branch release back into a newer main: +Release Please's chronological history cutoff can omit unreleased mainline +commits. Unreleased breaking markers require a major increment and references +in the new release's breaking-change migration section. Other omitted entries +are reported for review. Historical notes from an older release cannot satisfy +the new release's migration check. This audits declared markers and references; +it does not prove API compatibility or the quality of a migration explanation. + +If preparation reports a mismatch or omitted breaking changes, inspect the +recorded ancestry report, correct the release PR's version artifacts and notes, +regenerate documentation, and review/test its final head. Do not repeatedly +prepare over manual corrections. Publication independently checks the merged +candidate again before Release Please can create a tag. + +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. + +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. + +## 3. Publish the reviewed merge + +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. + +The workflow builds the bundled Windows controls and verifies the packaged +artifact before npm publication. It then verifies the published CLI, the +capability-scoped MCP catalog, and the isolated package update path before +publishing MCP Registry metadata. + +Record these outcomes separately: + +- GitHub release and tag, including the resolved commit SHA. +- npm's exact version and integrity, with the published-package canary passed. +- MCP Registry's exact server version and matching npm package version. + +A GitHub tag alone does not establish npm or MCP Registry publication. + +## 4. Sync metadata back to main + +Post-release synchronization is part of completing the release. Finish it +before cutting the next checkpoint; otherwise main retains the previous +release baseline and can propose an already-published version again. + +After publication, open a PR from the release branch back to main. Preserve +main's later implementation changes and regenerate ignored build outputs from +the combined contracts with the released package version. +Review and test this synchronization PR, then use a merge commit so the release +tag remains in main's ancestry. Keep the released tag unchanged. + +The synchronization must include `.release-please-manifest.json`, +`package.json`, both root versions in `package-lock.json`, `CHANGELOG.md`, +`server.json`, and the versioned documentation examples. Run +`npm run docs:generate`, `npm run docs:check`, and the release configuration +tests against the combined tree. After the merge, verify that the released +tag is an ancestor of main: + +```bash +git fetch origin main --tags +git merge-base --is-ancestor rea-agents-5.0.0 origin/main +``` + +Close any superseded rolling release PR. Future releases repeat the checkpoint +procedure from main; never resume automatic release-PR refreshes on main pushes. +Preparation generates and validates the catalog, portable conformance +projections, and packaged skill from the candidate checkout without committing +them. The checkpoint validator binds tracked version authority to the source +SHA; generated-catalog and package checks validate the derived representation. +No generated-metadata workflow pushes follow-up commits onto feature or release +branches. + +## Partial publication and retries + +Inspect the failed job and public registry state before retrying. When npm is +already published, the npm job verifies that exact version instead of +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. + +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. diff --git a/docs/roadmap.md b/docs/roadmap.md index a5f86686ca..cd78ee103b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -20,6 +20,30 @@ validation, provider prerequisites, target identity, cancellation, and owned resource cleanup remain part of each tool's contract. Setup still discloses installation changes and requires approval before writing or installing. +## Planned work + +The [website guides](https://morluto.github.io/rea/guides/) and +[generated catalog](product-catalog.json) describe available workflows. +Current development priorities are: + +- Keep generated metadata and narrative documentation aligned with tools, + providers, setup options and releases. +- Expand native architecture, type and indirect-call verification across + Hopper and Ghidra. +- Connect additional static extractors and runtime observations to + cross-layer feature traces. +- Improve obfuscated .NET comparisons and links between managed findings + and verified native analysis. +- Extend process, protocol, filesystem, reconnect and build-comparison + coverage, plus browser and Electron scenario actions. +- Evaluate native runtime observation through LLDB, Frida, system logs and + API tracing, and additional tools and targets such as Binary Ninja, Rizin, + LIEF, Windows-native workflows, mobile applications and firmware. + +Provider additions and platform support require the corresponding real +verification lanes; see [testing](testing.md) and +[provider evaluation](provider-evaluation.md). + ## Remaining evidence and provider work The [platform roadmap](https://github.com/morluto/rea/issues/32), @@ -62,7 +86,7 @@ Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites and exact commands. Experimental Windows x64 P0 now supplies bundled Job Object ownership, -protected runtime DACLs, and handle-based admission for native x86-64 PE +protected runtime DACLs, and handle-based admission for native x86 and x86-64 PE applications on local NTFS. Real ordinary-user CLI/MCP verification covers all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md) describes this boundary and unverified broader coverage. diff --git a/docs/testing.md b/docs/testing.md index 5c310539fb..7abebdae83 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -6,10 +6,27 @@ from real captured inputs. Keep focused module tests for distinct failure or semantic cases that these workflows cannot reliably reproduce. A test's path or suite name does not establish its behavioral depth. +Avoid tests that mirror getters, enum mappings, fixture helpers, or prescribed +call sequences. Prefer one representative workflow over a Cartesian matrix +when every row crosses the same boundary. Keep distinct command handlers, +producer representations, failure reasons, and lifecycle states covered. +Consolidate their shared setup without hiding the inputs or expected evidence. + Tests are grouped into Vitest projects so ownership, allowed dependencies and -runtime cost are visible from their paths. Before pruning a test, identify the -replacement scenario and its assertions; passing success journeys do not -replace malformed input, cancellation, permission or cleanup coverage. +runtime cost are visible from their paths. Before pruning a distinct boundary +regression, identify the replacement scenario and its assertions; passing +success journeys do not replace malformed input, cancellation, permission or +cleanup coverage. Trivial helper checks do not need a replacement. If only +tests consume a production module, trace CLI/MCP and verifier imports, +including imports of compiled files, and remove abandoned scaffolding with its +tests when it has no runtime consumer. + +Measure slow files before removing capacity regressions. Optimize repeated +process startup, fixture construction, and production algorithms when they +dominate runtime. Keep at least one input beyond each formerly failing size or +depth; smaller fixtures do not establish that truncation or stack exhaustion +has been fixed. Record the toolchain, coverage mode, and cache conditions when +comparing durations. ## Behavioral depths @@ -87,7 +104,16 @@ extension, a login item, a privileged helper, launchd plists, and a helper tool, signed ad hoc. It packs the app as a directory, a `ditto` ZIP, and an APFS DMG, then checks that `inspect-artifact` plus `project-apple-application-graph` report the same bundle anatomy for all three through the CLI, with stdio MCP -parity. It also checks that the DMG is detached afterwards. It runs in macOS CI. +parity. It also checks that the DMG is detached afterwards. The same app +covers `trace-dylib-resolution`: + +- each resolution status and finding, with CLI/MCP parity; +- for every traced image, dependencies, rpaths, and install names equal to + `otool -l`; +- for the main executable and an XPC service, a predicted load order equal to + the images dyld actually loads under `DYLD_PRINT_LIBRARIES`. + +It runs in macOS CI. Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle builder under `scripts/fixtures/apple/` and NIB byte fixtures beside the decoder @@ -103,6 +129,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 @@ -171,7 +206,7 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build. | `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites | -| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact | +| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact | Windows native conformance runs with `npm run verify:windows-native` and does not require Ghidra or Java. An optional independently compiled Windows fixture @@ -494,6 +529,17 @@ conformance/vtable fixture. `positive_e2e: false`; it must not be reported as capture/action proof. Both commands reject a changed executable digest and clean up the fixture process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md) + +`npm run verify:native-calls` needs only macOS with Command Line Tools (`clang`, +`lldb`, `codesign`, `nm`). It compiles `tests/conformance/native/calls.m` and +runs `observe-native-calls` through the CLI and stdio MCP. It checks: + +- the receiver class, selector and argument registers of every entry, and that + breakpoint addresses equal `nm`'s symbol addresses; +- captured stdout and an environment override; +- the event-limit and duration outcomes, with the process confirmed gone; +- that a hardened-runtime copy is refused with `debugger-attach-denied`, and + that the same copy signed with `get-task-allow` is traced. for the exact ABI, authority, graph and observation boundaries. ### Firmware adapters @@ -552,6 +598,18 @@ conditional `real-web-source-map` CI job supplies Chrome and an isolated pinned fixture compiler; static/unit checks do not acquire a browser. See [the source location guide](web-source-location.md) for the verified decoder profile. +### JavaScript large-output lane + +`npm run verify:javascript:output` exercises the CLI JSON result surface beyond +the running Node engine's single-string limit. Shared input leaves keep the +fixture's graph small; the verifier writes one temporary output file, checks its +complete byte count and an independent digest, then removes it. It requires only +Node and the built REA runtime, with space for the output plus a 1 GiB reserve. +Run `npm run verify:javascript:output -- jsonl` for compact JSONL coverage. This +opt-in lane is separate from routine tests and the canonical hash check +`npm run verify:javascript:digests`. It verifies serialization rather than an +arbitrary third-party application's parsing cost or MCP client capacity. + ### Website runtime attribution lane `npm run verify:browser:runtime` uses caller-supplied @@ -567,3 +625,75 @@ same checks through an isolated installed package. The conditional compiler. Ordinary unit/static gates acquire no browser. See [website runtime attribution](web-runtime.md) for effects, resource bounds and coverage limits. + +### Offline binary layout + +`npm run verify:binary:layout` requires Linux x64, GCC/binutils, absolute +`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and +absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF +fixtures and checks public CLI/MCP, lossless addresses/names, file ranges, +mitigation inferences, malformed/unsupported input, original file hashes and +released process ownership. Exec syscall tracing must identify only the declared +Node/Python launchers; no target binary is executed. Core/debugger claims need +separate verification lanes. Pass an installed package entrypoint as the script's +first argument to verify packaging independently of the checkout. +The valid SHN_XINDEX fixture has 65,281 full section rows; CLI is checked in +the ordinary lane. Its large MCP transfer is opt-in with +`REA_VERIFY_LARGE_ELF_MCP=1` (or the workflow dispatch `large_mcp` input), an +explicit 256 MiB SDK receive buffer and five-minute request timeout. Ordinary +MCP fixtures retain the pinned SDK defaults. + +### Offline EVM interface + +`npm run verify:evm:interface` requires Linux x64, an absolute +`REA_VERIFY_STRACE_COMMAND`, caller-supplied util-linux `prlimit` and `REA_VERIFY_SOLC_MODULE` selecting the absolute module path for +solc 0.8.30. It compiles source-owned plain/optimized/via-IR Cancun fixtures in +private storage and checks actual CLI/MCP selector evidence, raw/hex identity, +unknowns, malformed carriers and independent cleanup. An optional positional +entrypoint verifies a fresh installed package. It acquires no engine, compiler +or chain dependency and does not execute a contract on a chain. + +### Recorded crash evidence + +`npm run verify:recorded:crash` is a separate Linux x64 lane. It requires GCC, +GDB, absolute `REA_PWNTOOLS_PYTHON` with the offline ELF profile above, +`REA_PWNDBG_GDBINIT` and `REA_PWNDBG_VENV_PATH` with unchanged pwndbg 2026.09.15, +and `REA_VERIFY_STRACE_COMMAND`. Its disposable CI runner installs GDB, checks out the exact upstream +commit and installs its frozen lockfile in isolated runner storage. No developer +host configuration or core-pattern setting changes. + +Fixture generation explicitly runs an owned source-built two-thread program +under GDB to create a recording. Subsequent public CLI/MCP inspection verifies +lossless high registers, signed signals, note source bytes, malformed/missing +notes, unfamiliar owners, optional core-only mapping context and actionable +missing/unsupported plugin errors. A historical-PID collision fixture references +an owned live sentinel; inspection syscall traces reject process attach/memory +access, provider lookups of that PID's `/proc` files and attempted Internet sockets. Traces admit +the observed upstream startup helpers (`iconv -l`, the selected checkout's Git +version lookup) and REA ownership inspection separately from target execution. +This is fixture evidence, not a sandbox claim. Inputs remain unchanged and the +sentinel must stay alive; owned cleanup and empty verifier descendants are required. Pass an +installed package entrypoint as the script's first argument for package coverage. + +## Agent evaluation and conformance records + +Evaluate native, JavaScript, managed and browser investigation tasks through a +real local Codex CLI with: + +```bash +npm run verify:agent +``` + +Its report records tool selection, repeated calls, token use, completion quality, +and handling of permissions and unknowns. + +Regenerate the managed conformance manifest and Evidence completion ledger from +live verification results, or check them for drift: + +```bash +npm run evidence:generate +npm run evidence:check +``` + +The records preserve unsupported and unverified coverage as explicit unknowns. +Run the matching real-tool prerequisites described in this guide. diff --git a/docs/user-visible-errors.csv b/docs/user-visible-errors.csv index 1fa26521df..1adbcbbbc8 100644 --- a/docs/user-visible-errors.csv +++ b/docs/user-visible-errors.csv @@ -56,7 +56,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed 47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test 48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test -49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests +49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests 50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests 51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test 52,src/application/Setup.ts:approval,Setup plan lacks approval,"Review the setup plan, then rerun interactively or with --yes.",Low risk; exact authorization recovery,Keep current copy,unchanged,verified by setup unit tests @@ -67,13 +67,13 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 57,src/application/Setup.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test 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+, and 64-bit Arch Linux.",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 +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/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 diff --git a/docs/verification/managed-conformance-ledger.json b/docs/verification/managed-conformance-ledger.json deleted file mode 100644 index 0476f20a40..0000000000 --- a/docs/verification/managed-conformance-ledger.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "ledger_id": "ecl_132e857a8a93f0b366cd72f9cfa1c338ffd4100460a7834ef69abff936c84472", - "records": [ - { - "claim_id": "managed.application-graph-projection", - "status": "pass", - "evidence_ids": [ - "ev_b061a5a559acb3748cb9784c637f6693f74faeb564ba2b11d6fc32619e757459" - ] - }, - { - "claim_id": "managed.decompiler-reconstruction-import", - "status": "pass", - "evidence_ids": [ - "ev_13f3401a3aaddc3f6d087ad160fa928374e142cdb9861254359b46865b8c992e" - ] - }, - { - "claim_id": "managed.dotnet-framework-x86-pinvoke", - "status": "pass", - "evidence_ids": [ - "ev_6f026216826ca06e080b77ff608f02634fa945fbd2e151b4cd261c1444f8bbb1" - ] - }, - { - "claim_id": "managed.ilspy-reconstruction-oracle", - "status": "unsupported", - "evidence_ids": [ - "ev_65a94c4de03f9c6ebdaff3f381cca377821f349d3ca6dfad47a31cc586fae939" - ] - }, - { - "claim_id": "managed.malformed-metadata", - "status": "pass", - "evidence_ids": [ - "ev_deae4259ea4395623176de40d06c2abcbc2b3a77f0bfb068d76e568d801f45a8" - ] - }, - { - "claim_id": "managed.managed-native-verification", - "status": "pass", - "evidence_ids": [ - "ev_0edcbbcaa1191a4e5324a437f739957b77f9ac45e7897724337f2d6d06b3f9d1" - ] - }, - { - "claim_id": "managed.modern-dotnet-anycpu", - "status": "pass", - "evidence_ids": [ - "ev_d1af50e8dc04cd589385769efd8758e82095425f6e9c1cc4bf2f530c6b434c93" - ] - }, - { - "claim_id": "managed.mvid-and-token-drift", - "status": "pass", - "evidence_ids": [ - "ev_5493f40bc875535488d789a490361abcc1f76bfbdf52536d09ff127442a8e854" - ] - }, - { - "claim_id": "managed.not-managed", - "status": "pass", - "evidence_ids": [ - "ev_03ce0cc0183ee0710f8120e837565129559ab3bc41927df326066370c2341699" - ] - }, - { - "claim_id": "managed.operator-local-manifest", - "status": "unsupported", - "evidence_ids": [ - "ev_9d3e50da1d372c55f46649e69eae702cbe8f0ba6a81d1e46afc3e1a284c81757" - ] - }, - { - "claim_id": "managed.operator-manifest-self-test", - "status": "pass", - "evidence_ids": [ - "ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a" - ] - }, - { - "claim_id": "managed.unicode-obfuscated-identifiers", - "status": "pass", - "evidence_ids": [ - "ev_c4f38ce76d8359d92ee8e9442503e47b7913550442349a0df688bd274a3def67" - ] - }, - { - "claim_id": "managed.x64-ready-to-run-native-body", - "status": "pass", - "evidence_ids": [ - "ev_3e05ef1d6a796d3d4cb3372fe8e549efec759c59bf09d8ff660059a2e448cbc0" - ] - } - ], - "summary": { - "total": 13, - "pass": 11, - "fail": 0, - "unsupported": 2, - "truncated": 0, - "unknown": 0, - "complete": false - } -} diff --git a/docs/verification/managed-conformance-manifest.json b/docs/verification/managed-conformance-manifest.json deleted file mode 100644 index 5dc891091c..0000000000 --- a/docs/verification/managed-conformance-manifest.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "manifest_id": "ecm_2b2934cec1e1a386eed57f0a8f9cbd385ae22eaa63331179676482fc3a94f0a7", - "verifier": { - "id": "rea-managed-conformance", - "version": "1" - }, - "environment": { - "platform": "portable", - "architecture": "portable", - "runtime": "node", - "runtime_version": "portable" - }, - "skill_digests": [ - { - "skill_id": "reverse-engineer-anything", - "sha256": "ba6f172d69b5c92132be05df0260e545f6e99b5d8e6105d2e6c2ab270a9ae269" - } - ], - "claims": [ - { - "claim_id": "managed.application-graph-projection", - "scenario": { - "id": "managed-application-graph-projection", - "version": 1 - }, - "artifact_sha256s": [ - "f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.decompiler-reconstruction-import", - "scenario": { - "id": "decompiler-reconstruction-import", - "version": 1 - }, - "artifact_sha256s": [ - "cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.dotnet-framework-x86-pinvoke", - "scenario": { - "id": "dotnet-framework-x86-pinvoke", - "version": 1 - }, - "artifact_sha256s": [ - "f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.ilspy-reconstruction-oracle", - "scenario": { - "id": "ilspy-reconstruction-oracle", - "version": 1 - }, - "artifact_sha256s": [], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.malformed-metadata", - "scenario": { - "id": "malformed-metadata", - "version": 1 - }, - "artifact_sha256s": [ - "827e10b6391d29edca48c191e2ddd79f3bc1165e46e4ba2f4edf41ab60f45e78" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.managed-native-verification", - "scenario": { - "id": "managed-native-verification", - "version": 1 - }, - "artifact_sha256s": [ - "9999999999999999999999999999999999999999999999999999999999999999", - "f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.modern-dotnet-anycpu", - "scenario": { - "id": "modern-dotnet-anycpu", - "version": 1 - }, - "artifact_sha256s": [ - "92b588ae43d418117fa108c0004b5d0451f6112238d21bad8d75c0eb1da860f0" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.mvid-and-token-drift", - "scenario": { - "id": "mvid-and-token-drift", - "version": 1 - }, - "artifact_sha256s": [ - "276395c671a656ab57d583158443c10de552dccfb37dc97cd013f673c0f86b43", - "ef009dab7424fdeba996f2524dbe86fd899c96394498cca0463fcfdb228c5572" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.not-managed", - "scenario": { - "id": "not-managed", - "version": 1 - }, - "artifact_sha256s": [ - "de74dab8ab034054a811d4248efcda0bed505187a061a806e274ae87bc453754" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.operator-local-manifest", - "scenario": { - "id": "operator-local-manifest", - "version": 1 - }, - "artifact_sha256s": [], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.operator-manifest-self-test", - "scenario": { - "id": "operator-manifest-self-test", - "version": 1 - }, - "artifact_sha256s": [ - "cdc11ec7fd0f392e43b9b23971cb41ec49fd0a997fc33c24fcc553cd4386ae37" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.unicode-obfuscated-identifiers", - "scenario": { - "id": "unicode-obfuscated-identifiers", - "version": 1 - }, - "artifact_sha256s": [ - "cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.x64-ready-to-run-native-body", - "scenario": { - "id": "x64-ready-to-run-native-body", - "version": 1 - }, - "artifact_sha256s": [ - "e47402428322cdcb06f561036d7d108528cbc6e262d92a21f869218a20c80bfb" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - } - ] -} diff --git a/docs/web-network-captures.md b/docs/web-network-captures.md new file mode 100644 index 0000000000..01c2041c6f --- /dev/null +++ b/docs/web-network-captures.md @@ -0,0 +1,147 @@ +# Historical web network captures + +Use `inspect_web_network_capture` / `inspect-web-network-capture` to inspect +retained producer evidence. This is an offline operation: recorded URLs are +never fetched, and no browser or proxy listener is started. + +```sh +rea inspect-web-network-capture /absolute/session.har har --json +rea inspect-web-network-capture /absolute/session.har har --record 2 --record 0 --json +``` + +```json +{ + "name": "inspect_web_network_capture", + "arguments": { + "capture_path": "/absolute/session.har", + "format": "har", + "record_ordinals": [2, 0] + } +} +``` + +Omit ordinals to return every record. Explicit selection preserves caller order +and original zero-based producer ordinals; duplicate or out-of-range selections +are input errors. Every record is validated before selection, so selecting one +entry does not hide a malformed entry elsewhere. + +## Formats and upstream profiles + +HAR uses unchanged `har-schema@2.0.0` draft-06 schemas, `ajv@8.20.0`, +`ajv-formats@3.0.1`, `jsonc-parser@3.3.1` JSON visitor, and +`lossless-json@4.3.1` numeric representations, pinned in the npm lockfile. +Only the HAR 1.2 profile is supported. Unknown extensions remain reported +evidence. Duplicate object keys, including equal values and escaped spellings, +are rejected before materialization. Prototype-named members such as +`__proto__` are preserved as ordinary own JSON members without prototype +mutation. Original capture files remain unchanged. +The observed mitmproxy 12.2.3 `SaveHar` profile can emit `postData.text: null` +for a missing request body. REA preserves that reported null, omitting the +optional field only from its validation copy for this exact producer profile; +it does not invent an empty string or body bytes. + +Native mitmproxy decoding requires a caller-supplied **mitmdump 12.2.3 on +Linux**. Set its absolute executable path: + +```sh +REA_MITMDUMP_COMMAND=/absolute/mitmdump rea inspect-web-network-capture /absolute/session.mitm mitmproxy --json +``` + +REA does not install mitmproxy. Its owned process uses a private configuration +directory, `--no-server`, and the unchanged upstream `mitmproxy.io.tnetstring` +decoder. It does not run `FlowReader` migrations or convert native states to +HAR. Raw IDs, state versions, flow types, backup states and unknown extensions +are preserved without synthesizing connections, timestamps or content. The +tested upstream source is tag `v12.2.3`, commit +`6c09d56e4c29a92f5ad01b03199977584b8ea14f`; other native tool versions return +an explicit unsupported-profile diagnostic. Non-Linux native decoding is not +supported by this adapter. + +## Reading the evidence + +- `artifact` identifies the caller-selected file and stable snapshot digest. +- `records` preserve producer order/ordinals and original JSON pointers or byte + ranges. No live browser transaction or scenario IDs are invented. +- `reported` retains the producer's fields. HAR container metadata is returned + separately, with `log.entries` replaced by `null` and `records_pointer` + identifying the record collection. This is an inspection representation, + not a reconstructed HAR file. +- `numeric_literals` preserve HAR number lexemes and native integer/float + representations, including unsafe integers and non-finite native floats. + Values that cannot be safely represented as JSON numbers are `null` in the + reported view. Unsafe standard HAR numeric fields fail schema validation; + unsafe extension numbers remain available through their sidecars. +- `binary_fields` retain exposed bytes, base64 and SHA-256 independently of + declared sizes. Valid HAR `content.encoding: "base64"` is decoded exactly; + producer-decoded Unicode text, missing text and unknown encodings do not + establish original body bytes. Post-data parameters do not reconstruct a + multipart body. Native byte fields also have an exact UTF-8 display view + when valid; binary sidecars establish their actual representation. Native + missing content and empty content remain distinct. +- `redactions` identify known transport authentication exclusions and explicit + sensitive-value exclusions. Redacted byte fields retain neither original + bytes nor their digest. Unknown extension names and token-looking query + parameters do not establish sensitivity. + +Mark sensitive literal string/UTF-8 byte values with MCP `sensitive_values`, or +repeat CLI `--sensitive-value`. These declarations are never persisted in +Evidence parameters; the count is retained. Explicit text/UTF-8 byte matches +exclude the entire field with a reported `null` rather than substitute a text +marker or rewrite payload bytes. This prevents replacement markers from +reintroducing declared literals. The file digest identifies the original artifact +unless it matches an explicit declaration; in that case `artifact.sha256` and +Evidence `subject` are null with an explanation. A sensitive property name excludes that +entire property and its sidecars; a `scope: "property-name"` redaction points +to its actual parent. No substitute property name or child pointer is invented. +Omitted subtrees are still validated, including canonical HAR base64. Explicitly sensitive artifact +paths are empty in Evidence and omitted from locations, while the original +SHA-256 and size remain available unless the digest itself is explicitly excluded. Failure messages, diagnostics and cleanup +resources follow the same explicit declarations without changing error types; +sensitive issue pointers identify a real ancestor instead of a fabricated +coordinate. Header/cookie authentication values and known +transport URL userinfo are excluded structurally, including native backups. + +Generated free-text limitations follow the same literal declarations, including +global, per-record and Evidence explanations added during projection. Contract +field names, typed discriminators and exact decoder identities keep their +canonical meanings; declarations do not rename formats or providers. + +Historical evidence does not prove runtime attribution, execution or deployment +authenticity. Mitmproxy messages are upstream-assembled messages, not recovered +wire fragments. Native dictionary duplicate keys have the unchanged upstream +decoder's last-value semantics; this adapter does not claim to recover discarded +duplicates or the original numeric spelling of parsed native floats. + +## Resource and lifecycle boundaries + +Complete evidence has a 32 MiB input, 96 MiB decoder reply and 64-level nesting +budget. Exceeding a budget returns no partial success. +Oversized selected captures return an input error identifying `capture_path` +and the byte budget; owned reply/output failures remain output or provider errors. +HAR runs in an owned +192 MiB Node old-generation heap with one V8 worker. Native decoding applies +Linux limits of 768 MiB address space, 30 seconds CPU and 96 MiB file output; +both commands have a 30-second wall deadline and independently supervised +process cleanup. Owned decoder diagnostics retain at most 1 MiB across stdout +and stderr; the collector drops overflow as it arrives, reports the limit +failure and verifies cleanup. Heap limits alone do not establish aggregate RSS limits. + +Stable regular-file reads reject symlinks, replacement and concurrent changes. +Private snapshots, declarations and tool configuration are removed before the +result returns. Cleanup uncertainty retains the selected capture, resource and +original failure information. The original capture is never changed. + +## Excluded coordinates and read failures + +Explicitly marked values also exclude matching source coordinates and their sidecars. A hidden record coordinate is reported as `location.kind: "unknown"`; REA does not invent a replacement pointer. Original record ordinals and artifact digests remain available. + +Host filesystem read denials return `code: "access_denied"`, `category: "unavailable"`, the actual `EACCES`/`EPERM` code and read-access guidance. Missing files remain input failures. Cleanup failures retain the prior structured error, including its input constraints or provider diagnostics, with explicitly marked text excluded. + +An observed replacement or in-place change during stable artifact acquisition +returns `code: "artifact_changed"`, `category: "integrity_mismatch"` and +`retryable: true`, retaining the selected path and reader's reason. Wait until +the capture file is stable before retrying. No unstable snapshot is decoded. +Command cleanup failures retain the original status and both diagnostic streams; +the decoder's ownership preparation receives the same cancellation signal. + +MCP advertises the same strict named input contract used by the application. Historical inspection delegates argument validation to that shared application boundary so accepted explicit-sensitive declarations also cover invalid argument names and other correction details. The SDK receives raw arguments through its Standard Schema interface; malformed inputs still fail before decoder or filesystem effects and return the normal structured REA error. Protocol-envelope validation remains owned by MCP. diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md index f837bae09f..ec54f5908a 100644 --- a/docs/windows-ghidra-p0.md +++ b/docs/windows-ghidra-p0.md @@ -3,6 +3,8 @@ This guide describes repository main. npm 4.1.0 includes the Windows native bundle; check the [release boundary](installation.md#released-package-and-main) before applying these instructions to an older published package. +Native x86 (32-bit) PE support is available on repository main; npm releases +through 5.0.0 admit x86-64 PE targets on Windows. Status: experimental Windows x64 support for the read-only P0 boundary. Windows package builds bundle native process, filesystem, and DACL controls. REA @@ -15,7 +17,7 @@ additional permission flag or degraded mode is required. - Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+; - an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4); - a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases); -- an explicit native, non-managed, non-DLL x86-64 PE application; and +- an explicit native, non-managed, non-DLL x86 or x86-64 PE application; and - the 25 read-only Ghidra inventory, memory, and function-analysis operations. Loaded memory reads and file offsets preserve Ghidra's observed source mappings. @@ -98,7 +100,7 @@ and artifact SHA-256 before loading the package-owned addon. Native failure reasons remain distinct from a valid installation and an unsupported target. PE header classification and SHA-256 come from the same open file. The Windows -provider admits native x86-64 PE applications and rejects unsupported roles, +provider admits native x86 and x86-64 PE applications and rejects unsupported roles, architectures, managed images, and malformed headers. The original selected source coordinate is preserved for native admission. @@ -156,6 +158,8 @@ artifact and manifest. ```powershell npm run verify:windows-native npm run verify:ghidra:windows +npm run verify:ghidra:windows -- --x86 +npm run verify:ghidra:windows:package -- --x86 npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe ``` diff --git a/install.sh b/install.sh index 2df6548e28..d059694844 100755 --- a/install.sh +++ b/install.sh @@ -3,6 +3,10 @@ set -euo pipefail PACKAGE="rea-agents" REPOSITORY="morluto/rea" +numeric_identifier='(0|[1-9][0-9]*)' +prerelease_identifier="(${numeric_identifier}|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)" +build_identifier='[0-9A-Za-z-]+' +version_pattern="${numeric_identifier}\\.${numeric_identifier}\\.${numeric_identifier}(-${prerelease_identifier}(\\.${prerelease_identifier})*)?(\\+${build_identifier}(\\.${build_identifier})*)?" version="${REA_VERSION:-}" dry_run=false start_setup=true @@ -67,14 +71,20 @@ if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] || fi if [[ -n "$version" ]]; then - [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "version must be an exact semantic version." + [[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version." else release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry." tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION." - [[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION." + [[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION." version="${BASH_REMATCH[1]}" fi +# npm's exact-version resolver cleans build metadata from the requested version, +# so it may install the base release and make the post-install identity check fail. +if [[ "$version" =~ \+ ]]; then + fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry." +fi + prefix_args=() if [[ "$platform" == "Linux" ]]; then prefix_args=(--prefix "$HOME/.local") @@ -97,7 +107,7 @@ if [[ "$dry_run" == true ]]; then fi printf 'Installing %s@%s...\n' "$PACKAGE" "$version" -npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry." +npm install --global ${prefix_args[@]+"${prefix_args[@]}"} "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry." [[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry." installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry." [[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry." diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc index d50fc1e291..8d7ef7a6b5 100644 --- a/native/windows/src/filesystem.cc +++ b/native/windows/src/filesystem.cc @@ -277,14 +277,15 @@ static void directory(Runtime& root, const std::wstring& relative) { } } -static Handle createPrivateFile(Runtime& root, const std::wstring& relative) { +static Handle createPrivateFile(Runtime& root, const std::wstring& relative, + DWORD access = DELETE | GENERIC_WRITE | GENERIC_READ | READ_CONTROL) { const auto path = relativePath(root, relative); const auto parentPath = path.substr(0, path.find_last_of(L'\\')); auto parent = runtimeDirectory(root, parentPath); verifyPrivate(parent->get(), parentPath, parentPath == root.path); Security security; auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1), - GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path); + access, false, security, path); verifyPrivate(file.get(), path, false); return file; } @@ -305,6 +306,26 @@ static void dispose(HANDLE handle, const std::wstring& path) { require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path); } +static void disposeSnapshot(Runtime& root, Handle& file, const std::wstring& path) { + // Snapshot readers do not share DELETE access. Release the writer only for + // failed-copy cleanup, with its parent pinned and its identity recorded. + auto parent = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\'))); + FILE_ID_INFO expected{}; + require(GetFileInformationByHandleEx(file.get(), FileIdInfo, &expected, sizeof(expected)), + "Read incomplete snapshot identity failed", path); + file.reset(); + auto cleanup = openComponent(path, DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES, false); + FILE_ID_INFO observed{}; + require(GetFileInformationByHandleEx(cleanup.get(), FileIdInfo, &observed, sizeof(observed)), + "Read snapshot cleanup identity failed", path); + require(expected.VolumeSerialNumber == observed.VolumeSerialNumber && + std::equal(std::begin(expected.FileId.Identifier), std::end(expected.FileId.Identifier), + std::begin(observed.FileId.Identifier)), + "Incomplete snapshot identity changed before cleanup", path, ERROR_ACCESS_DENIED); + verifyPrivate(cleanup.get(), path, false); + dispose(cleanup.get(), path); +} + void closeRuntime(Runtime& root) { if (root.closed) return; // Windows keeps the bearer descriptor and snapshot leases until this owner @@ -412,6 +433,17 @@ static void copySnapshot(napi_env, void* pointer) { static void completeSnapshot(napi_env env, napi_status status, void* pointer) { std::unique_ptr work(static_cast(pointer)); work->root->snapshotPending = false; + const auto reject = [&](const Failure& failure) { + try { + disposeSnapshot(*work->root, work->destination, work->path); + napi_reject_deferred(env, work->deferred, failureValue(env, failure)); + } catch (const Failure& cleanupFailure) { + const Failure combined( + failure.constraint + "; incomplete snapshot cleanup failed: " + cleanupFailure.constraint, + work->path, cleanupFailure.win32); + napi_reject_deferred(env, work->deferred, failureValue(env, combined)); + } + }; try { if (work->failure) throw *work->failure; require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED); @@ -422,10 +454,9 @@ static void completeSnapshot(napi_env env, napi_status status, void* pointer) { work->root->immutableFiles.push_back(std::move(work->destination)); check(napi_resolve_deferred(env, work->deferred, result)); } catch (const Failure& failure) { - napi_reject_deferred(env, work->deferred, failureValue(env, failure)); + reject(failure); } catch (const std::exception& failure) { - napi_reject_deferred(env, work->deferred, - failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE))); + reject(Failure(failure.what(), work->path, ERROR_GEN_FAILURE)); } napi_delete_reference(env, work->rootReference); napi_delete_async_work(env, work->work); @@ -436,7 +467,9 @@ static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root, auto work = std::make_unique(env, root); work->source = openFile(sourcePath); work->path = relativePath(root, relative); - work->destination = createPrivateFile(root, relative); + // Retain the write lease with read-only sharing, but omit DELETE authority: + // Java's RandomAccessFile reader shares read/write, not delete access. + work->destination = createPrivateFile(root, relative, GENERIC_WRITE | GENERIC_READ | READ_CONTROL); napi_value promise; check(napi_create_promise(env, &work->deferred, &promise)); check(napi_create_reference(env, rootValue, 1, &work->rootReference)); @@ -506,7 +539,13 @@ napi_value filesystemCall(napi_env env, const std::wstring& operation, const std auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\'))); file->path = path; file->requestedPath = path; - file->handles.push_back(openComponent(path, GENERIC_READ, false)); + // Retained output handles have write and DELETE access, so this read open + // must share both to pass Windows' symmetric share check. Those retained + // handles still omit FILE_SHARE_WRITE and FILE_SHARE_DELETE, blocking + // competing writes and delete opens. + file->handles.push_back(openComponent(path, GENERIC_READ, false, + FILE_SHARE_READ | FILE_SHARE_WRITE | + FILE_SHARE_DELETE)); verifyPrivate(file->get(), path, false); auto result = identity(env, file->get(), path); set(env, result, "handle", wrap(env, std::move(file))); diff --git a/package-lock.json b/package-lock.json index f1c2520dc9..efa3ef62a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rea-agents", - "version": "4.1.0", + "version": "6.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rea-agents", - "version": "4.1.0", + "version": "6.0.0", "license": "MIT", "dependencies": { "@babel/parser": "8.0.4", @@ -19,14 +19,20 @@ "@modelcontextprotocol/server": "2.3.1", "@xterm/addon-serialize": "0.14.0", "@xterm/headless": "6.0.0", - "@zip.js/zip.js": "2.8.26", + "@zip.js/zip.js": "2.15.0", + "ajv": "8.20.0", + "ajv-formats": "3.0.1", "canonicalize": "3.0.0", "cross-spawn": "7.0.6", "diff": "9.0.0", + "evmole": "0.9.3", + "har-schema": "2.0.0", "ignore": "7.0.6", "incur": "0.6.1", "isomorphic-git": "1.38.7", "jsonc-parser": "3.3.1", + "lossless-json": "4.3.1", + "parse5": "8.0.1", "pino": "10.4.0", "playwright-core": "1.58.2", "plist": "5.0.0", @@ -48,7 +54,6 @@ "@types/write-file-atomic": "4.0.3", "@types/ws": "8.18.1", "@vitest/coverage-v8": "5.0.3", - "ajv": "8.20.0", "husky": "9.1.7", "jscpd": "5.0.12", "knip": "6.39.0", @@ -3945,9 +3950,9 @@ ] }, "node_modules/@zip.js/zip.js": { - "version": "2.8.26", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", - "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.15.0.tgz", + "integrity": "sha512-hYAuHAaWjt0axbofaDL5XUlmrQPsBDcK3f45ApZuh6N+8UGVk26GoGdtGDvcvzahsHytTQ6DtBVXwG9IcCMjjQ==", "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", @@ -3971,7 +3976,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3984,6 +3988,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/algoliasearch": { "version": "5.59.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.59.0.tgz", @@ -4623,6 +4644,18 @@ "node": ">=18.0.0" } }, + "node_modules/evmole": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/evmole/-/evmole-0.9.3.tgz", + "integrity": "sha512-UhO5HMAoEPCupSxvBJPlm/L0Qk/KVJ/OJo3r4EyhXADIEHqU5aTkAzsBfzk4lXGjqxVnaYbh1GPSmZtmSHFnQQ==", + "license": "MIT", + "bin": { + "evmole": "dist/evmole_cli.mjs" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -4660,7 +4693,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-string-truncated-width": { @@ -4682,7 +4714,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", - "dev": true, "funding": [ { "type": "github", @@ -4890,6 +4921,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -5315,7 +5355,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/jsonc-parser": { @@ -5743,6 +5782,12 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/lossless-json": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.1.tgz", + "integrity": "sha512-SqD/Bg3ZfltBJ2Z14hJ/BihnvtV553WO4g9/ePtlp4lrnl9jF3AdIJt53A/Wkg/0Li+LMfxaBqgx1MiFZdQlpQ==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -6308,6 +6353,30 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6609,7 +6678,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" diff --git a/package.json b/package.json index df0c7f3621..26fffefde4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rea-agents", "mcpName": "io.github.morluto/rea", - "version": "4.1.0", + "version": "6.0.0", "description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.", "license": "MIT", "repository": { @@ -44,10 +44,14 @@ "bridge/hopper_bridge.py", "bridge/native/NativeUIChildren.swift", "bridge/native/ReaNativeUI.swift", + "bridge/native/rea_lldb_tracer.py", + "bridge/process/ProcessRunTokenReader.swift", + "bridge/process/ProcessRunTokenReaderMain.swift", "bridge/ghidra/ReaGhidraBridge.java", "bridge/ghidra/ReaGhidraExtensions.java", "bridge/ghidra/ReaGhidraPrepareCom.java", "bridge/android/ReaJadxBridge.java", + "bridge/mitmproxy/capture.py", "scripts/rea.mjs", "scripts/electron-active-hook.cjs", "scripts/electron-active-hook-boundaries.cjs", @@ -59,18 +63,31 @@ "LICENSE", "native/windows/build/manifest.json", "native/windows/build/rea-windows-x64.node", - "scripts/verify-windows-native-artifact.mjs" + "scripts/verify-windows-native-artifact.mjs", + "bridge/pwntools/layout.py", + "bridge/pwntools/layout_impl.py", + "third_party/pwntools/README.md", + "third_party/evmole/README.md", + "third_party/evmole/LICENSE", + "bridge/pwntools/decoder_runtime.py", + "bridge/pwntools/recorded_crash.py", + "bridge/pwntools/recorded_crash_impl.py", + "bridge/pwndbg/launch.py", + "bridge/pwndbg/launch_impl.py", + "bridge/pwndbg/core_context.py", + "third_party/pwndbg/README.md", + "third_party/pwndbg/LICENSE.md" ], "scripts": { "deps:check": "node scripts/check-dependency-install.mjs", "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 && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs", "precheck": "npm run deps:check", "check": "turbo run typecheck lint format:check knip metadata:check", "precheck:fast": "npm run deps:check", @@ -83,7 +100,7 @@ "check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos", "precheck:ci": "npm run deps:check", "check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos", - "build:cached": "turbo run build", + "build:cached": "node scripts/check-dependency-install.mjs && turbo run build", "format": "oxfmt .", "format:check": "oxfmt --check .", "lint": "oxlint && npm run verify:module-boundaries", @@ -141,6 +158,7 @@ "verify:inspector": "npm run build:cached && node scripts/verify/inspector/runtime-observation.mjs", "verify:javascript-runtime": "npm run build:cached && node scripts/verify/javascript/runtime-observation.mjs", "verify:javascript:digests": "npm run build:cached && node scripts/verify/javascript/digests.mjs", + "verify:javascript:output": "npm run build:cached && node scripts/verify/javascript/output.mjs", "verify:application-workflows": "npm run build:cached && node scripts/verify/javascript/application-workflows.mjs", "verify:managed": "npm run build:cached && node scripts/verify/managed/conformance.mjs", "verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs", @@ -161,17 +179,20 @@ "config:print": "node scripts/print-mcp-config.mjs", "evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs", "evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check", + "predocs:dev": "npm run docs:generate", "docs:dev": "vitepress dev docs", + "predocs:build": "npm run docs:generate", "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", "prepack": "node scripts/prepack.mjs", "prepare": "node scripts/prepare.mjs", "verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs", + "verify:native-calls": "npm run build:cached && node scripts/verify-native-calls.mjs", "verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial", "test:focused": "node scripts/run-development-tests.mjs focused", "build:windows-native": "node scripts/build-windows-native.mjs", @@ -188,7 +209,11 @@ "verify:javascript:recovery": "node scripts/verify/javascript/recovery.mjs", "verify:module-boundaries": "node scripts/verify-module-boundaries.mjs", "verify:browser:source-maps": "npm run build:cached && node scripts/verify-browser-source-maps.mjs", - "verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs" + "verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs", + "verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs", + "verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs", + "verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs", + "verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs" }, "lint-staged": { "*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [ @@ -208,14 +233,19 @@ "@modelcontextprotocol/server": "2.3.1", "@xterm/addon-serialize": "0.14.0", "@xterm/headless": "6.0.0", - "@zip.js/zip.js": "2.8.26", + "@zip.js/zip.js": "2.15.0", + "ajv": "8.20.0", + "ajv-formats": "3.0.1", "canonicalize": "3.0.0", "cross-spawn": "7.0.6", "diff": "9.0.0", + "har-schema": "2.0.0", "ignore": "7.0.6", "incur": "0.6.1", "isomorphic-git": "1.38.7", "jsonc-parser": "3.3.1", + "lossless-json": "4.3.1", + "parse5": "8.0.1", "pino": "10.4.0", "playwright-core": "1.58.2", "plist": "5.0.0", @@ -223,7 +253,8 @@ "smol-toml": "1.9.0", "write-file-atomic": "7.0.0", "ws": "8.21.0", - "zod": "4.4.3" + "zod": "4.4.3", + "evmole": "0.9.3" }, "overrides": { "vitepress": { @@ -238,7 +269,6 @@ "@types/write-file-atomic": "4.0.3", "@types/ws": "8.18.1", "@vitest/coverage-v8": "5.0.3", - "ajv": "8.20.0", "husky": "9.1.7", "jscpd": "5.0.12", "knip": "6.39.0", diff --git a/release-please-config.json b/release-please-config.json index c1ac99aa0a..4560b53fa0 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -16,7 +16,6 @@ ".": { "package-name": "rea-agents", "extra-files": [ - { "type": "generic", "path": "README.md" }, { "type": "generic", "path": "README_zh.md" }, { "type": "generic", "path": "README_ja.md" }, { "type": "generic", "path": "README_ko.md" }, @@ -35,11 +34,6 @@ "type": "json", "path": "server.json", "jsonpath": "$.packages[0].version" - }, - { - "type": "json", - "path": "docs/product-catalog.json", - "jsonpath": "$.package.version" } ] } diff --git a/scripts/create-ghidra-windows-fixture.mjs b/scripts/create-ghidra-windows-fixture.mjs index fcb1df5f2b..6538ec57f8 100644 --- a/scripts/create-ghidra-windows-fixture.mjs +++ b/scripts/create-ghidra-windows-fixture.mjs @@ -3,50 +3,68 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const output = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe"); -const bytes = Buffer.alloc(1024); -const peOffset = 0x80; -const optionalHeader = peOffset + 24; -const sectionTable = optionalHeader + 0xf0; +for (const architecture of ["x86_64", "x86"]) { + const x86 = architecture === "x86"; + const output = resolve( + root, + "build", + "fixtures", + x86 ? "rea-ghidra-windows-x86.exe" : "rea-ghidra-windows.exe", + ); + const bytes = Buffer.alloc(1024); + const peOffset = 0x80; + const optionalHeader = peOffset + 24; + const optionalHeaderSize = x86 ? 0xe0 : 0xf0; + const sectionTable = optionalHeader + optionalHeaderSize; -bytes.write("MZ", 0, "ascii"); -bytes.writeUInt32LE(peOffset, 0x3c); -bytes.write("PE\0\0", peOffset, "binary"); -bytes.writeUInt16LE(0x8664, peOffset + 4); -bytes.writeUInt16LE(1, peOffset + 6); -bytes.writeUInt16LE(0xf0, peOffset + 20); -bytes.writeUInt16LE(0x22, peOffset + 22); + bytes.write("MZ", 0, "ascii"); + bytes.writeUInt32LE(peOffset, 0x3c); + bytes.write("PE\0\0", peOffset, "binary"); + bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4); + bytes.writeUInt16LE(1, peOffset + 6); + bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20); + bytes.writeUInt16LE(x86 ? 0x102 : 0x22, peOffset + 22); -bytes.writeUInt16LE(0x20b, optionalHeader); -bytes.writeUInt32LE(0x200, optionalHeader + 4); -bytes.writeUInt32LE(0x1000, optionalHeader + 16); -bytes.writeUInt32LE(0x1000, optionalHeader + 20); -bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24); -bytes.writeUInt32LE(0x1000, optionalHeader + 32); -bytes.writeUInt32LE(0x200, optionalHeader + 36); -bytes.writeUInt16LE(6, optionalHeader + 40); -bytes.writeUInt16LE(6, optionalHeader + 48); -bytes.writeUInt32LE(0x2000, optionalHeader + 56); -bytes.writeUInt32LE(0x200, optionalHeader + 60); -bytes.writeUInt16LE(3, optionalHeader + 68); -bytes.writeUInt16LE(0x8160, optionalHeader + 70); -bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72); -bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80); -bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88); -bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96); -bytes.writeUInt32LE(16, optionalHeader + 108); + bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader); + bytes.writeUInt32LE(0x200, optionalHeader + 4); + bytes.writeUInt32LE(0x1000, optionalHeader + 16); + bytes.writeUInt32LE(0x1000, optionalHeader + 20); + if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28); + else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24); + bytes.writeUInt32LE(0x1000, optionalHeader + 32); + bytes.writeUInt32LE(0x200, optionalHeader + 36); + bytes.writeUInt16LE(6, optionalHeader + 40); + bytes.writeUInt16LE(6, optionalHeader + 48); + bytes.writeUInt32LE(0x2000, optionalHeader + 56); + bytes.writeUInt32LE(0x200, optionalHeader + 60); + bytes.writeUInt16LE(3, optionalHeader + 68); + bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70); + if (x86) { + bytes.writeUInt32LE(0x10_0000, optionalHeader + 72); + bytes.writeUInt32LE(0x1000, optionalHeader + 76); + bytes.writeUInt32LE(0x10_0000, optionalHeader + 80); + bytes.writeUInt32LE(0x1000, optionalHeader + 84); + bytes.writeUInt32LE(16, optionalHeader + 92); + } else { + bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72); + bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80); + bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88); + bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96); + bytes.writeUInt32LE(16, optionalHeader + 108); + } -bytes.write(".text", sectionTable, "ascii"); -bytes.writeUInt32LE(0x16, sectionTable + 8); -bytes.writeUInt32LE(0x1000, sectionTable + 12); -bytes.writeUInt32LE(0x200, sectionTable + 16); -bytes.writeUInt32LE(0x200, sectionTable + 20); -bytes.writeUInt32LE(0x6000_0020, sectionTable + 36); + bytes.write(".text", sectionTable, "ascii"); + bytes.writeUInt32LE(0x16, sectionTable + 8); + bytes.writeUInt32LE(0x1000, sectionTable + 12); + bytes.writeUInt32LE(0x200, sectionTable + 16); + bytes.writeUInt32LE(0x200, sectionTable + 20); + bytes.writeUInt32LE(0x6000_0020, sectionTable + 36); -// entry calls the second function, returns zero; second returns 42. -bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200); -bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210); + // entry calls the second function, returns zero; second returns 42. + bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200); + bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210); -await mkdir(dirname(output), { recursive: true }); -await writeFile(output, bytes); -process.stdout.write(`${output}\n`); + await mkdir(dirname(output), { recursive: true }); + await writeFile(output, bytes); + if (!x86) process.stdout.write(`${output}\n`); +} diff --git a/scripts/fixtures/apple/macos-bundle.mjs b/scripts/fixtures/apple/macos-bundle.mjs index de2dcae812..adb1a2c108 100644 --- a/scripts/fixtures/apple/macos-bundle.mjs +++ b/scripts/fixtures/apple/macos-bundle.mjs @@ -138,7 +138,11 @@ const char *core_chain(void) { return sibling_value() == 7 ? chain_variant() : " return join(version, "Core"); }; -/** The main executable, with a weak dependency deleted after linking. */ +/** + * The main executable. It searches a missing `Overrides` rpath before + * `Frameworks`, weakly links a library deleted after linking, and + * delay-links a library, which the linker encodes as `dylib_use_command`. + */ const buildMainExecutable = async (root, contents, coreBinary) => { const gone = join(contents, "Frameworks", "libgone.dylib"); await clang(root, { @@ -147,22 +151,33 @@ const buildMainExecutable = async (root, contents, coreBinary) => { output: gone, flags: ["-dynamiclib", "-install_name", "@rpath/libgone.dylib"], }); + const delayed = join(contents, "Frameworks", "libdelay.dylib"); + await clang(root, { + name: "delay", + source: `int delay_value(void) { return 3; }\n`, + output: delayed, + flags: ["-dynamiclib", "-install_name", "@rpath/libdelay.dylib"], + }); const executable = join(contents, "MacOS", "MacFixture"); await clang(root, { name: "main", source: `#include const char *core_chain(void); +int delay_value(void); extern int gone_value(void) __attribute__((weak_import)); int main(void) { + if (delay_value() != 3) return 1; printf("chain=%s weak=%s\\n", core_chain(), gone_value ? "present" : "absent"); return 0; } `, output: executable, flags: [ + "-Wl,-rpath,@executable_path/../Overrides", "-Wl,-rpath,@executable_path/../Frameworks", coreBinary, `-Wl,-weak_library,${gone}`, + `-Wl,-delay_library,${delayed}`, ], }); await rm(gone); @@ -191,7 +206,12 @@ const buildNestedBundles = async (root, contents, coreBinary) => { int main(void) { return core_chain()[0] == 'b'; } `, output: join(service, "MacOS", "Svc"), - flags: ["-Wl,-rpath,@executable_path/../../../../Frameworks", coreBinary], + // An absolute rpath outside the bundle precedes the bundled framework. + flags: [ + "-Wl,-rpath,/opt/rea-fixture-absent/lib", + "-Wl,-rpath,@executable_path/../../../../Frameworks", + coreBinary, + ], }); await write( join(service, "Info.plist"), diff --git a/scripts/fixtures/binary-layout-high.S b/scripts/fixtures/binary-layout-high.S new file mode 100644 index 0000000000..5faf3fcb78 --- /dev/null +++ b/scripts/fixtures/binary-layout-high.S @@ -0,0 +1,10 @@ +.globl _start +.globl absolute_value +.set absolute_value, 0x20000000000007 +.bss +.lcomm zero_backing, 8192 +.text +_start: + mov $60, %eax + xor %edi, %edi + syscall diff --git a/scripts/fixtures/binary-layout.c b/scripts/fixtures/binary-layout.c new file mode 100644 index 0000000000..0ff882b8ed --- /dev/null +++ b/scripts/fixtures/binary-layout.c @@ -0,0 +1,17 @@ +#include +#include + +int stored_value = 7; +char zero_backing[8192]; +__thread int tls_value; +int common_value; + +__attribute__((noinline)) int read_values(const char *text) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%s", text); + return buffer[0] + stored_value + tls_value + common_value; +} + +int main(int argc, char **argv) { + return read_values(argc > 1 ? argv[1] : "source-owned"); +} diff --git a/scripts/fixtures/evm-interface.sol b/scripts/fixtures/evm-interface.sol new file mode 100644 index 0000000000..396479172d --- /dev/null +++ b/scripts/fixtures/evm-interface.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +// Public source-owned fixture; generated runtime bytecode is never tracked. +contract InterfaceFixture { + uint256 public stored; + function set(uint256 value) external { stored = value; } + function read() external view returns (uint256) { return stored; } + function pair(uint32 a, address b) external pure returns (uint32, address) { return (a, b); } + receive() external payable {} + fallback() external {} +} diff --git a/scripts/fixtures/generate-mitmproxy-capture.py b/scripts/fixtures/generate-mitmproxy-capture.py new file mode 100644 index 0000000000..4be3dd4a73 --- /dev/null +++ b/scripts/fixtures/generate-mitmproxy-capture.py @@ -0,0 +1,151 @@ +import importlib.util +import json +from pathlib import Path +from mitmproxy import connection, ctx, http +from mitmproxy.io import FlowWriter +from mitmproxy.io import tnetstring +from mitmproxy.addons.savehar import SaveHar +from mitmproxy.websocket import WebSocketData, WebSocketMessage + +class Generator: + def load(self, loader): + loader.add_option("rea_fixture_root", str, "", "Owned synthetic fixture root") + loader.add_option("rea_adapter_path", str, "", "REA native adapter under verification") + + def running(self): + root = Path(ctx.options.rea_fixture_root) + spec = importlib.util.spec_from_file_location("rea_capture_under_test", ctx.options.rea_adapter_path) + adapter = importlib.util.module_from_spec(spec) + spec.loader.exec_module(adapter) + classifications = [ + (adapter.CaptureFailure("format", "Malformed retained record.", "/record"), "format"), + (adapter.CaptureFailure("input-limit", "Nesting budget.", "/extension"), "input-limit"), + (MemoryError("payload must not enter diagnostics"), "resource-limit"), + (RuntimeError("payload must not enter diagnostics"), "decoder"), + (OSError("payload must not enter diagnostics"), "decoder"), + ] + for error, expected in classifications: + reply = adapter.failure_reply(error) + assert reply["reason"] == expected, reply + assert "payload must not enter diagnostics" not in json.dumps(reply) + for state, expected in [({"metadata": {1: b"bad-key"}}, "format"), ({"metadata": object()}, "decoder")]: + try: + adapter.project_record(state, []) + except adapter.CaptureFailure as error: + assert error.reason == expected, (error.reason, expected) + assert error.pointer == "/metadata", error.pointer + else: + raise AssertionError("Invalid native representation was accepted") + non_utf8 = adapter.project_record({"request": {"headers": [(b"Referer", b"https://byte-user:byte-secret@example.test/path\xff")] }}, []) + assert non_utf8["reported"]["request"]["headers"][0][1] is None, non_utf8 + assert non_utf8["binary_fields"][-1]["state"] == "redacted", non_utf8 + for value in [" \tHTTPS://ows-user:ows-secret@example.test/path\t ", b" \tHTTPS://ows-user:ows-secret@example.test/path\t "]: + projected = adapter.project_record({"request": {"headers": [(b"Referer", value)]}}, []) + assert projected["reported"]["request"]["headers"][0][1] == " \tHTTPS://example.test/path\t ", projected + assert "ows-secret" not in json.dumps(projected), projected + if isinstance(value, bytes): + assert projected["binary_fields"][-1]["state"] == "redacted", projected + (root / "native-error-classifications.json").write_text(json.dumps({"passed": len(classifications) + 2})) + first = http.HTTPFlow(connection.Client(peername=("127.0.0.1", 1), sockname=("127.0.0.1", 2)), connection.Server(address=("example.test", 80))) + first.id = "original-producer-id" + first.request = http.Request.make("POST", "http://example.test/a?token=ordinary", b"\x00\xffbody", [(b"Authorization", b"Bearer native-transport-secret"), (b"X-Duplicate", b"one"), (b"X-Duplicate", b"two")]) + first.request.path = "http://user:password@example.test/a?token=ordinary#fragment" + first.response = http.Response.make(200, b"\x00\xfeanswer", [(b"Set-Cookie", b"session=cookie-secret"), (b"Location", b"https://redirect-user:redirect-password@example.test/b?token=ordinary")]) + first.metadata = {"big_integer": 9007199254740993, "nonfinite": float("inf"), "isLosslessNumber": True, "value": "ordinary-extension", "response": {"headers": [["Authorization", "ordinary-extension-credential-name"]]}} + first.websocket = WebSocketData(messages=[WebSocketMessage(2, True, b"\x00\xfdmessage", 123.5)]) + first.backup() + first.comment = "caller-selected-fixture" + second = first.copy() + second.id = first.id + second.request.raw_content = None + second.response.raw_content = b"" + second.websocket = None + with (root / "flows.mitm").open("wb") as handle: + writer = FlowWriter(handle) + writer.add(first) + writer.add(second) + har_first, har_second = first.copy(), second.copy() + har_first.request.path = "/a?token=ordinary#fragment" + har_second.request.path = "/a?token=ordinary#fragment" + har_first.response.raw_content = b"\x00\xff\x01\xfe" + markers = {"sensitive": "REDACTED", "bracket": "literal[", "overlap": "secret", "ordinary": "unmarked"} + har = SaveHar().make_har([har_first, har_second]) + har["log"]["entries"][0]["_markers"] = markers + har["log"]["entries"][0]["_private_properties"] = {"private-property/~": {"number": 123, "text": "private-property"}, "kept": 7} + (root / "producer.har").write_text(json.dumps(har)) + har_text = json.dumps(har) + for name, members in [ + ("equal-number", '"_duplicate": 1, "_duplicate": 1'), + ("equal-null", '"_duplicate": null, "_duplicate": null'), + ("equal-object", '"_duplicate": {"value": 1}, "_duplicate": {"value": 1}'), + ("escaped-key", '"_duplicate": 1, "\\u005fduplicate": 1'), + ]: + duplicate = har_text.replace('"cache": {}', '"cache": {}, ' + members, 1) + assert duplicate != har_text + (root / ("duplicate-" + name + ".har")).write_text(duplicate) + duplicate = har_text.replace('"version": "1.2"', '"version": "1.2", "version": "1.2"', 1) + assert duplicate != har_text + (root / "duplicate-core.har").write_text(duplicate) + prototype = json.loads(har_text) + prototype["_extension"] = {"__proto__": {"preserved": 7}, "constructor": "ordinary"} + (root / "prototype.har").write_text(json.dumps(prototype)) + prototype_native = first.get_state() + prototype_native["metadata"]["__proto__"] = {"preserved": 7} + with (root / "prototype.mitm").open("wb") as handle: + tnetstring.dump(prototype_native, handle) + invalid_har = json.loads(json.dumps(har)) + invalid_har["log"]["entries"][0]["response"]["content"].update({"encoding": "base64", "text": "AR=="}) + (root / "invalid-private-parent.har").write_text(json.dumps(invalid_har)) + strings = first.get_state() + for state in [strings, strings["backup"]]: + state["request"]["path"] = "https://native-user:string-password@example.test/string-path" + state["request"]["authority"] = "authority-user:authority-password@example.test" + state["request"]["headers"] = [(b"Referer", "https://referer-user:referer-password@example.test/from"), (b"Origin", "//origin-user:origin-password@example.test")] + state["response"]["headers"] = [(b"Location", "https://location-user:location-password@example.test/to")] + state["metadata"]["_markers"] = dict(markers) + state["metadata"]["_private_properties"] = {"private-property/~": {"number": 123, "bytes": b"private-property"}, "kept": 7} + with (root / "string-urls.mitm").open("wb") as handle: + tnetstring.dump(strings, handle) + strings["metadata"]["_private_properties"]["private-property/~"] = {1: b"unsupported-key"} + with (root / "invalid-private-key.mitm").open("wb") as handle: + tnetstring.dump(strings, handle) + extension = "leaf" + for _ in range(66): + extension = {"child": extension} + har["_extension"] = extension + (root / "deep.har").write_text(json.dumps(har)) + deep = first.get_state() + deep["_extension"] = extension + with (root / "deep.mitm").open("wb") as handle: + tnetstring.dump(deep, handle) + for name, value in [("invalid-credential-key", {1: b"bad-key"}), ("deep-credential", extension)]: + malformed = first.get_state() + malformed["request"]["headers"] = [(b"Authorization", value)] + with (root / (name + ".mitm")).open("wb") as handle: + tnetstring.dump(malformed, handle) + for prefix, name in [(" \t", "ows"), ("\r\n \v\f\x00\x1f", "controls")]: + url = prefix + "HTTPS://ows-user:ows-secret@example.test/path?token=ordinary#fragment\t " + relative = prefix + "//ows-user:ows-secret@example.test/path?token=ordinary#fragment\t " + whitespace_har = json.loads((root / "producer.har").read_text()) + entry = whitespace_har["log"]["entries"][0] + for side, header in [("response", "Location"), ("request", "Referer"), ("request", "Origin")]: + entry[side]["headers"] = [{"name": header, "value": url}] + entry[side]["headers"] + (root / (name + ".har")).write_text(json.dumps(whitespace_har)) + whitespace_native = first.get_state() + for state in [whitespace_native, whitespace_native["backup"]]: + state["request"]["headers"] = [(b"Referer", url.encode("utf-8")), (b"Origin", relative)] + state["response"]["headers"] = [(b"Location", url)] + with (root / (name + ".mitm")).open("wb") as handle: + tnetstring.dump(whitespace_native, handle) + non_utf8_native = first.get_state() + for state in [non_utf8_native, non_utf8_native["backup"]]: + state["request"]["path"] = b"https://byte-user:byte-secret@example.test/path\xff" + state["request"]["authority"] = b"byte-user:byte-secret@example.test\xff" + state["request"]["headers"] = [(b"Referer", b" https://byte-user:byte-secret@example.test/from\xff"), (b"Origin", b"//byte-user:byte-secret@example.test\xff")] + state["response"]["headers"] = [(b"Location", b"https://byte-user:byte-secret@example.test/to\xff")] + with (root / "non-utf8-urls.mitm").open("wb") as handle: + tnetstring.dump(non_utf8_native, handle) + (root / "oracle.json").write_text(json.dumps({"records": 2, "request_base64": "AP9ib2R5", "response_base64": "AP5hbnN3ZXI=", "websocket_base64": "AP1tZXNzYWdl", "id": first.id})) + ctx.master.shutdown() + +addons = [Generator()] diff --git a/scripts/fixtures/recorded-crash.c b/scripts/fixtures/recorded-crash.c new file mode 100644 index 0000000000..a7baa7da1a --- /dev/null +++ b/scripts/fixtures/recorded-crash.c @@ -0,0 +1,24 @@ +#include +#include +#include +#include +static atomic_int ready; +static void *recorded_thread(void *unused) { + (void)unused; + __asm__ volatile("movabs $0xfedcba9876543210, %%r12" ::: "r12"); + atomic_store(&ready, 1); + for (;;) pause(); + return 0; +} +__attribute__((noinline)) static void fixture_crash(uint64_t value) { + volatile uint64_t *pointer = (volatile uint64_t *)(uintptr_t)0x123; + *pointer = value; +} +int main(void) { + pthread_t thread; + pthread_attr_t attr; + if (pthread_attr_init(&attr) || pthread_attr_setstacksize(&attr, 65536) || pthread_create(&thread, &attr, recorded_thread, 0)) return 1; + while (!atomic_load(&ready)) usleep(1000); + fixture_crash(UINT64_C(0x1122334455667788)); + return 0; +} diff --git a/scripts/generate-completion-ledger.mjs b/scripts/generate-completion-ledger.mjs index fa3bf6b4a0..ce63ffd40c 100644 --- a/scripts/generate-completion-ledger.mjs +++ b/scripts/generate-completion-ledger.mjs @@ -30,7 +30,7 @@ const { stdout } = await exec( }, ); const verifierOutput = JSON.parse(stdout); -// Managed conformance claims are host-independent. Keep the checked-in +// Managed conformance claims are host-independent. Keep the generated // commitment reproducible across macOS, Linux, and Windows rather than // embedding the machine that happened to generate it. const portableCompletionReport = { diff --git a/scripts/generate-mcp-tool-catalog.mjs b/scripts/generate-mcp-tool-catalog.mjs index 7d44af653a..4803cf8d27 100644 --- a/scripts/generate-mcp-tool-catalog.mjs +++ b/scripts/generate-mcp-tool-catalog.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( diff --git a/scripts/generate-package-metadata.mjs b/scripts/generate-package-metadata.mjs index 2a23368397..8247dac1c2 100644 --- a/scripts/generate-package-metadata.mjs +++ b/scripts/generate-package-metadata.mjs @@ -17,7 +17,7 @@ const lock = JSON.parse( await readFile(join(root, "package-lock.json"), "utf8"), ); const skill = await readFile( - join(root, "skills/reverse-engineer-anything/SKILL.md"), + join(root, "skill-src/reverse-engineer-anything/SKILL.md"), "utf8", ); const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1]; diff --git a/scripts/generate-product-catalog.mjs b/scripts/generate-product-catalog.mjs index c13e80c3e2..fa6aa4ad8f 100644 --- a/scripts/generate-product-catalog.mjs +++ b/scripts/generate-product-catalog.mjs @@ -1,3 +1,4 @@ +import { mkdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -14,8 +15,10 @@ for (const argument of arguments_) const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const catalog = await createProductCatalog(root); +const path = join(root, "docs/public/product-catalog.json"); +if (!arguments_.has("--check")) await mkdir(dirname(path), { recursive: true }); await ensureGeneratedFile({ - path: join(root, "docs/product-catalog.json"), + path, source: await serializeProductCatalog(catalog), check: arguments_.has("--check"), generateCommand: "npm run docs:generate", diff --git a/scripts/generate-skill-metadata.mjs b/scripts/generate-skill-metadata.mjs index 3d4bc4bbca..490976fb51 100644 --- a/scripts/generate-skill-metadata.mjs +++ b/scripts/generate-skill-metadata.mjs @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { mkdir, readFile, readdir, rm } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -10,26 +10,69 @@ for (const argument of arguments_) throw new Error(`Unknown skill metadata option: ${argument}`); const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const path = join(root, "skills/reverse-engineer-anything/SKILL.md"); -const current = await readFile(path, "utf8"); +const sourceRoot = join(root, "skill-src/reverse-engineer-anything"); +const outputRoot = join(root, "skills/reverse-engineer-anything"); +const check = arguments_.has("--check"); +const current = ( + await readFile(join(sourceRoot, "SKILL.md"), "utf8") +).replaceAll("\r\n", "\n"); const { CATALOG_IDENTITY } = await import( `${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}` ); -const withCount = current.replace( - /^\s{2}tool_count:\s*\d+\s*$/mu, - ` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`, +if (/^\s{2}(?:tool_count|catalog_digest):/mu.test(current)) + throw new Error( + "Catalog metadata belongs in the generated skill, not skill-src", + ); +const versionLine = /^ {2}version: "[^"\r\n]+"$/mu; +if (!versionLine.test(current)) + throw new Error("Missing authored skill version"); +const source = current.replace( + versionLine, + `$&\n tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`, ); -const digestLine = ` catalog_digest: "${CATALOG_IDENTITY.digests.combined_sha256}"`; -const source = /^\s{2}catalog_digest:/mu.test(withCount) - ? withCount.replace( - /^\s{2}catalog_digest:\s*"[a-f0-9]{64}"\s*$/mu, - digestLine, - ) - : withCount.replace(/^(\s{2}tool_count:\s*\d+\s*)$/mu, `$1\n${digestLine}`); +const paths = await filePaths(sourceRoot); +// Rebuild this owned output directory so removed references cannot survive a build. +if (!check) await rm(outputRoot, { recursive: true, force: true }); +for (const relativePath of paths) { + const path = join(outputRoot, relativePath); + if (!check) await mkdir(dirname(path), { recursive: true }); + await ensureGeneratedFile({ + path, + source: + relativePath === "SKILL.md" + ? source + : (await readFile(join(sourceRoot, relativePath), "utf8")).replaceAll( + "\r\n", + "\n", + ), + check, + generateCommand: "npm run docs:generate", + }); +} +if ( + check && + JSON.stringify(await filePaths(outputRoot)) !== JSON.stringify(paths) +) + throw new Error( + "Generated skill file inventory drifted; run npm run docs:generate", + ); -await ensureGeneratedFile({ - path, - source, - check: arguments_.has("--check"), - generateCommand: "npm run docs:generate", -}); +async function filePaths(directory, prefix = "") { + const paths = []; + for (const entry of (await readdir(directory, { withFileTypes: true })).sort( + (left, right) => + left.name < right.name ? -1 : left.name > right.name ? 1 : 0, + )) { + const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`; + if (entry.isDirectory()) + paths.push( + ...(await filePaths(join(directory, entry.name), relativePath)), + ); + else if (entry.isFile()) paths.push(relativePath); + else + throw new Error( + `Skill bundle requires regular files or directories: ${relativePath}`, + ); + } + return paths; +} diff --git a/scripts/hopper-demo-x11.py b/scripts/hopper-demo-x11.py index 936f8e9e72..470c9fd28f 100644 --- a/scripts/hopper-demo-x11.py +++ b/scripts/hopper-demo-x11.py @@ -24,8 +24,10 @@ SUPPORTED_HOPPER_SHA256 = { "1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13", } EXPECTED_SCREEN = (1280, 1024) -EXPECTED_DIALOG = (189, 370, 901, 284) -DEMO_CLICK = (305, 632) +DIALOG_TITLES = {"Registration", "Registration — Hopper Disassembler"} +HOPPER_WINDOW_TITLE = "Hopper Disassembler" +DEMO_CLICK_LEFT = 116 +DEMO_CLICK_BOTTOM = 22 PRIVATE_DISPLAY_UNAVAILABLE = 70 X11_AUTHORIZATION_FAILED = 71 UNSUPPORTED_HOPPER_BUILD = 72 @@ -197,6 +199,47 @@ def windows(x11: ctypes.CDLL, display: int, root: int) -> list[tuple[int, int, i return found +def window_title(x11: ctypes.CDLL, display: int, window: int) -> str | None: + title = ctypes.c_char_p() + if not x11.XFetchName(display, window, ctypes.byref(title)) or not title.value: + return None + try: + return title.value.decode("utf-8", "replace") + finally: + x11.XFree(ctypes.cast(title, ctypes.c_void_p)) + + +def window_transient_for( + x11: ctypes.CDLL, display: int, window: int +) -> int | None: + parent = ctypes.c_ulong() + if not x11.XGetTransientForHint(display, window, ctypes.byref(parent)): + return None + return int(parent.value) + + +def find_demo_dialog( + x11: ctypes.CDLL, + display: int, + observed: list[tuple[int, int, int, int, int]], +) -> tuple[int, int, int, int, int] | None: + for candidate in observed: + window = candidate[0] + if window_title(x11, display, window) not in DIALOG_TITLES: + continue + parent = window_transient_for(x11, display, window) + if parent is None: + continue + if window_title(x11, display, parent) == HOPPER_WINDOW_TITLE: + return candidate + return None + + +def demo_click(dialog: tuple[int, int, int, int, int]) -> tuple[int, int]: + _window, x, y, _width, height = dialog + return x + DEMO_CLICK_LEFT, y + height - DEMO_CLICK_BOTTOM + + def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None: window_pointer = ctypes.POINTER(ctypes.c_ulong) x11.XOpenDisplay.argtypes = [ctypes.c_char_p] @@ -213,6 +256,18 @@ def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None: ctypes.POINTER(XWindowAttributes), ] x11.XGetWindowAttributes.restype = ctypes.c_int + x11.XFetchName.argtypes = [ + ctypes.c_void_p, + ctypes.c_ulong, + ctypes.POINTER(ctypes.c_char_p), + ] + x11.XFetchName.restype = ctypes.c_int + x11.XGetTransientForHint.argtypes = [ + ctypes.c_void_p, + ctypes.c_ulong, + ctypes.POINTER(ctypes.c_ulong), + ] + x11.XGetTransientForHint.restype = ctypes.c_int x11.XQueryTree.argtypes = [ ctypes.c_void_p, ctypes.c_ulong, @@ -645,6 +700,7 @@ def run_launch(args: argparse.Namespace) -> int: deadline = time.monotonic() + 10 observed: list[tuple[int, int, int, int, int]] = [] + dialog: tuple[int, int, int, int, int] | None = None while time.monotonic() < deadline: if socket.exists(): x11.XCloseDisplay(display) @@ -660,7 +716,8 @@ def run_launch(args: argparse.Namespace) -> int: "owned Hopper process exited", ) observed = windows(x11, display, root) - if any(window[1:] == EXPECTED_DIALOG for window in observed): + dialog = find_demo_dialog(x11, display, observed) + if dialog is not None: break time.sleep(0.1) else: @@ -676,8 +733,10 @@ def run_launch(args: argparse.Namespace) -> int: "expected Hopper demo dialog not found", ) + assert dialog is not None + click_x, click_y = demo_click(dialog) input_results = ( - xtst.XTestFakeMotionEvent(display, -1, DEMO_CLICK[0], DEMO_CLICK[1], 0), + xtst.XTestFakeMotionEvent(display, -1, click_x, click_y, 0), xtst.XTestFakeButtonEvent(display, 1, True, 0), xtst.XTestFakeButtonEvent(display, 1, False, 0), ) @@ -696,9 +755,8 @@ def run_launch(args: argparse.Namespace) -> int: finally: if display is not None: x11.XCloseDisplay(display) - if child is not None and child.poll() is None: - child.terminate() - child.wait() + if child is not None: + stop_process(child) stop_process(xvfb) capture.close() diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 1e5ddfab6b..8049261cd6 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -16,12 +16,18 @@ export const toolFamilyCatalog = (sources) => { { id: "native", surface: "native-provider", - contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS, + contracts: [ + ...sources.nativeContracts.NATIVE_TOOL_CONTRACTS, + ...sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS, + ], }, { id: "artifact", surface: "artifact-provider", - contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS, + contracts: [ + ...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS, + ...sources.evmContracts.EVM_TOOL_CONTRACTS, + ], }, { id: "managed", @@ -48,6 +54,8 @@ export const toolFamilyCatalog = (sources) => { ...sources.browserContracts.BROWSER_TOOL_CONTRACTS, ...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS, ...sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS, + ...sources.webNetworkCaptureContracts + .WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, ], }, { @@ -141,6 +149,15 @@ export const providerCatalog = (sources) => { identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY, contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS, }, + { + identity: sources.pwntoolsProvider.PWNTOOLS_PROVIDER_IDENTITY, + contracts: + sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS, + }, + { + identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY, + contracts: sources.evmContracts.EVM_TOOL_CONTRACTS, + }, { identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER, contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS, @@ -170,6 +187,17 @@ export const providerCatalog = (sources) => { identity: sources.androidProvider.JADX_PROVIDER_IDENTITY, contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS, }, + { + identity: sources.webNetworkCaptureProvider.HAR_CAPTURE_PROVIDER_IDENTITY, + contracts: + sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, + }, + { + identity: + sources.webNetworkCaptureProvider.MITMPROXY_CAPTURE_PROVIDER_IDENTITY, + contracts: + sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, + }, { identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY, contracts: [ diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs index 311fb5f0a5..9dcfdbb187 100644 --- a/scripts/lib/catalog-core.mjs +++ b/scripts/lib/catalog-core.mjs @@ -40,7 +40,12 @@ const SOURCE_PATHS = { officialContracts: "dist/contracts/officialToolContracts.js", enhancedContracts: "dist/contracts/enhancedToolContracts.js", sessionContracts: "dist/contracts/sessionToolContracts.js", + binaryDiagnosticsContracts: + "dist/contracts/native/binaryDiagnosticsToolContracts.js", + pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js", nativeContracts: "dist/contracts/native/nativeToolContracts.js", + evmContracts: "dist/contracts/evm/evmToolContracts.js", + evmProvider: "dist/evm/EvmoleRelease.js", artifactContracts: "dist/contracts/artifactToolContracts.js", managedContracts: "dist/contracts/managed/managedToolContracts.js", firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js", @@ -51,6 +56,9 @@ const SOURCE_PATHS = { "dist/contracts/managed/managedWorkflowToolContracts.js", browserContracts: "dist/contracts/browserToolContracts.js", webRuntimeContracts: "dist/contracts/webRuntimeToolContracts.js", + webNetworkCaptureContracts: + "dist/contracts/webNetworkCaptureToolContracts.js", + webNetworkCaptureProvider: "dist/browser/history/CaptureRelease.js", browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js", electronContracts: "dist/contracts/javascript/electronToolContracts.js", javascriptRuntimeObservationContracts: @@ -75,7 +83,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", diff --git a/scripts/lib/docs-facts.mjs b/scripts/lib/docs-facts.mjs index 32e6fbb541..d45d545aa7 100644 --- a/scripts/lib/docs-facts.mjs +++ b/scripts/lib/docs-facts.mjs @@ -75,6 +75,16 @@ export const documentationFactIssues = async (root, catalog) => { for (const path of README_PATHS) { const content = await readFile(join(root, path), "utf8"); requireText(issues, path, content, "MCP-tool_catalog"); + if (path === "README.md") { + requireText(issues, path, content, "(docs/installation.md"); + requireText( + issues, + path, + content, + "(docs/mcp-contracts.md#generated-catalog)", + ); + continue; + } for (const client of catalog.setup_clients) requireText(issues, path, content, client.display_name); try { @@ -88,15 +98,23 @@ export const documentationFactIssues = async (root, catalog) => { } } + const installationPath = "docs/installation.md"; + const installation = await readFile(join(root, installationPath), "utf8"); + for (const client of catalog.setup_clients) + requireText(issues, installationPath, installation, client.display_name); + const agents = await readFile(join(root, "AGENTS.md"), "utf8"); - requireText(issues, "AGENTS.md", agents, "docs/product-catalog.json"); + requireText(issues, "AGENTS.md", agents, "docs/public/product-catalog.json"); const templatePath = ".github/pull_request_template.md"; const template = await readFile(join(root, templatePath), "utf8"); - requireText(issues, templatePath, template, "docs/product-catalog.json"); + requireText( + issues, + templatePath, + template, + "docs/public/product-catalog.json", + ); - const english = await readFile(join(root, "README.md"), "utf8"); - requireText(issues, "README.md", english, "docs/product-catalog.json"); return issues; }; diff --git a/scripts/lib/elf-section-name-fixtures.mjs b/scripts/lib/elf-section-name-fixtures.mjs new file mode 100644 index 0000000000..ad3684c845 --- /dev/null +++ b/scripts/lib/elf-section-name-fixtures.mjs @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; + +/** Source-owned ELF64 mutations for declared, absent and escaped name-table references. */ +export const sectionNameFixtures = (original, report) => { + // ELF gABI: https://gabi.xinuos.com/elf/02-eheader.html#e-shstrndx + // SHN_XINDEX must carry an actual index >= SHN_LORESERVE, not a small index. + const sourceTable = report.sections[original.readUInt16LE(62)]; + assert.equal(sourceTable.type, "SHT_STRTAB"); + const sourceHeader = Number(BigInt(sourceTable.header_location.offset)); + const fixtures = []; + for (const problem of [ + "out-of-range", + "wrong-type", + "undeclared-header", + "extended-undeclared-header", + "extended-without-sections", + "extended-absent", + "extended-small", + ]) { + let bytes = Buffer.from(original); + let selected = report.sections.length + 100; + if (problem === "wrong-type") + selected = report.sections.find( + (section) => section.type === "SHT_PROGBITS", + ).index; + else if (problem.includes("undeclared-header")) { + const end = + Number(bytes.readBigUInt64LE(40)) + + report.sections.length * bytes.readUInt16LE(58); + const extended = Buffer.alloc(Math.max(bytes.length, end + 64)); + bytes.copy(extended); + bytes.copy(extended, end, sourceHeader, sourceHeader + 64); + bytes = extended; + selected = report.sections.length; + } + if (problem === "extended-without-sections") { + bytes.writeBigUInt64LE(0n, 40); + bytes.writeUInt16LE(0, 60); + bytes.writeUInt16LE(0xffff, 62); + } else if (problem.startsWith("extended-")) { + bytes.writeUInt16LE(0xffff, 62); + if (problem === "extended-absent") selected = 0; + if (problem === "extended-small") selected = sourceTable.index; + bytes.writeUInt32LE(selected, Number(bytes.readBigUInt64LE(40)) + 40); + } else bytes.writeUInt16LE(selected, 62); + fixtures.push({ name: problem, bytes, expectation: "invalid_input" }); + } + const absent = Buffer.from(original); + absent.writeUInt16LE(0, 62); + fixtures.push({ name: "absent", bytes: absent, expectation: "absent" }); + + // A genuine escape requires a large table. Relocate the original table, keep + // its referenced indices and move only the name table to index SHN_LORESERVE. + const selected = 0xff00, + count = selected + 1; + const start = Math.ceil(original.length / 8) * 8; + const large = Buffer.alloc(start + count * 64); + original.copy(large); + const oldStart = Number(original.readBigUInt64LE(40)); + original.copy(large, start, oldStart, oldStart + report.sections.length * 64); + large.fill( + 0, + start + sourceTable.index * 64, + start + (sourceTable.index + 1) * 64, + ); + original.copy(large, start + selected * 64, sourceHeader, sourceHeader + 64); + large.writeBigUInt64LE(BigInt(start), 40); + large.writeUInt16LE(0, 60); + large.writeUInt16LE(0xffff, 62); + large.writeBigUInt64LE(BigInt(count), start + 32); + large.writeUInt32LE(selected, start + 40); + fixtures.push({ + name: "extended-large", + bytes: large, + expectation: "resolved", + tableIndex: selected, + sectionCount: count, + }); + const reserved = Buffer.from(large); + reserved.writeUInt16LE(selected, 62); + reserved.writeUInt32LE(0, start + 40); + fixtures.push({ + name: "direct-reserved", + bytes: reserved, + expectation: "invalid_input", + }); + return fixtures; +}; diff --git a/scripts/lib/generated-file.mjs b/scripts/lib/generated-file.mjs index d7df714c60..e296210d0b 100644 --- a/scripts/lib/generated-file.mjs +++ b/scripts/lib/generated-file.mjs @@ -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 }; diff --git a/scripts/lib/large-result-mcp.mjs b/scripts/lib/large-result-mcp.mjs new file mode 100644 index 0000000000..d7a4ab30ff --- /dev/null +++ b/scripts/lib/large-result-mcp.mjs @@ -0,0 +1,27 @@ +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; + +/** Own one SDK client with an explicit receive budget for a large complete result. */ +export const withLargeResultMcp = async ( + { entrypoint, environment, maxBufferSize }, + useClient, +) => { + const client = new Client({ name: "large-result-verifier", version: "1" }); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: environment, + stderr: "inherit", + maxBufferSize, + }); + try { + await client.connect(transport); + return await useClient(client); + } finally { + try { + await client.close(); + } finally { + await transport.close(); + } + } +}; diff --git a/scripts/lib/module-boundaries.d.mts b/scripts/lib/module-boundaries.d.mts index 51d5957c6b..e8b6d2dd67 100644 --- a/scripts/lib/module-boundaries.d.mts +++ b/scripts/lib/module-boundaries.d.mts @@ -6,6 +6,7 @@ export interface ModuleBoundaryViolation { readonly target: string; readonly boundary: | "artifact-acquisition" + | "process-capture" | "pure-layer" | "application-composition" | "provider-construction" diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index a0c71738b4..d52a651f56 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const PURE_LAYERS = new Set(["domain", "contracts"]); const MIGRATED_PROVIDER_ROOTS = new Set([ + "evm", "android", "firmware", "ghidra", @@ -60,6 +61,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( @@ -70,6 +79,8 @@ const failedBoundary = (file, target) => { if ( (file.startsWith("src/artifacts/javascript/") || file.startsWith("src/artifacts/apple/") || + file.startsWith("src/artifacts/inventory/") || + file.startsWith("src/artifacts/extraction/") || /^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) && (["application", "composition", "server", "cli", "main"].includes( targetLayer, diff --git a/scripts/lib/product-catalog.mjs b/scripts/lib/product-catalog.mjs index 8c4aaae4f9..266e37291f 100644 --- a/scripts/lib/product-catalog.mjs +++ b/scripts/lib/product-catalog.mjs @@ -58,16 +58,16 @@ export const createProductCatalog = async (root) => { runtime_catalog: { counts: sources.catalogIdentity.CATALOG_IDENTITY.counts, digests: { - ...sources.catalogIdentity.CATALOG_IDENTITY.digests, + // Full runtime schema digests belong to server identity, not this facts projection. providers_sha256: providerCatalogDigest(providers), }, }, }; }; -/** Stable checked-in representation of the product catalog. */ +/** Stable build-generated representation of the product catalog. */ export const serializeProductCatalog = (catalog) => formatGeneratedFile( - "docs/product-catalog.json", + "docs/public/product-catalog.json", JSON.stringify(catalog, null, 2), ); diff --git a/scripts/lib/recorded-core-fixtures.py b/scripts/lib/recorded-core-fixtures.py new file mode 100644 index 0000000000..cc8d654439 --- /dev/null +++ b/scripts/lib/recorded-core-fixtures.py @@ -0,0 +1,94 @@ +"""Source-owned mutations built with the same unchanged upstream ELF/ABI structures.""" +import ctypes +import io +import json +from pathlib import Path +import sys +from elftools.elf.elffile import ELFFile +from elftools.construct import Container +from pwnlib.elf.datatypes import elf_prstatus_amd64 + +source, directory, sentinel = Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3]) +original = source.read_bytes() +image = ELFFile(io.BytesIO(original)) +segments = list(image.iter_segments()) +notes = [note for segment in segments if segment.header.p_type == "PT_NOTE" for note in segment.iter_notes()] +status = next(note for note in notes if note.n_name == "CORE" and note.n_type == "NT_PRSTATUS") +descriptor_start = lambda note: note.n_offset + image.structs.Elf_Nhdr.sizeof() + ((note.n_namesz + 3) // 4) * 4 +files = {} + + +def save(name, content): + path = directory / (name + ".core") + path.write_bytes(content) + files[name] = str(path) + + +def note_header(content, note, **changes): + header = dict(note) + header.update(changes) + encoded = image.structs.Elf_Nhdr.build(Container(**header)) + content[note.n_offset:note.n_offset + len(encoded)] = encoded + + +save("truncated-header", original[:32]) +for name, osabi, version in [("linux-osabi", 3, 0), ("solaris-osabi", 6, 0), ("freebsd-osabi", 9, 0), ("unsupported-abi-version", 0, 1)]: + content = bytearray(original) + content[7:9] = bytes([osabi, version]) + save(name, content) +content = bytearray(original) +section = Container(sh_name=0, sh_type="SHT_NULL", sh_flags=0, sh_addr=0, sh_offset=0, sh_size=0, sh_link=0, sh_info=len(segments), sh_addralign=0, sh_entsize=0) +content.extend(image.structs.Elf_Shdr.build(section)) +header = Container(**dict(image.header, e_phnum=0xffff, e_shoff=len(original), e_shentsize=image.structs.Elf_Shdr.sizeof(), e_shnum=1, e_shstrndx=0)) +content[:image.structs.Elf_Ehdr.sizeof()] = image.structs.Elf_Ehdr.build(header) +save("extended-program-count", content) +for index, segment in enumerate(segments): + if segment.header.p_type != "PT_NOTE": + continue + content = bytearray(original) + raw_notes = original[segment.header.p_offset:segment.header.p_offset + segment.header.p_filesz] + content.extend(raw_notes + b"\0\0\0") + header = Container(**dict(segment.header, p_offset=len(original), p_filesz=len(raw_notes) + 3)) + start = image.header.e_phoff + index * image.header.e_phentsize + content[start:start + image.structs.Elf_Phdr.sizeof()] = image.structs.Elf_Phdr.build(header) + save("zero-note-tail", content) + content[-1] = 1 + save("nonzero-note-tail", content) + break +content = bytearray(original) +note_header(content, status, n_descsz=1) +save("truncated-status", content) +content = bytearray(original) +note_header(content, status, n_namesz=len(original)) +save("truncated-owner", content) +content = bytearray(original) +content[status.n_offset + image.structs.Elf_Nhdr.sizeof()] = 255 +save("opaque-owner", content) +content = bytearray(original) +for note in notes: + if note.n_type == "NT_SIGINFO": + note_header(content, note, n_type=0x7ffeff) +save("unknown-signals", content) +content = bytearray(original) +for index, segment in enumerate(segments): + if segment.header.p_type == "PT_NOTE": + header = dict(segment.header, p_type="PT_NULL") + encoded = image.structs.Elf_Phdr.build(Container(**header)) + start = image.header.e_phoff + index * image.header.e_phentsize + content[start:start + len(encoded)] = encoded +save("missing-notes", content) +content = bytearray(original) +start = descriptor_start(status) + elf_prstatus_amd64.pr_pid.offset +content[start:start + ctypes.sizeof(ctypes.c_uint32)] = (0xfffffffe).to_bytes(4, "little") +save("negative-pid", content) +content = bytearray(original) +content[start:start + ctypes.sizeof(ctypes.c_uint32)] = sentinel.to_bytes(4, "little") +for note in notes: + if note.n_name == "CORE" and note.n_type == "NT_PRPSINFO": + # Build with unchanged pyelftools; no hard-coded PID offset or host file resolution. + raw = dict(note.n_desc, pr_pid=sentinel) + encoded = image.structs.Elf_Prpsinfo.build(Container(**raw)) + offset = descriptor_start(note) + content[offset:offset + len(encoded)] = encoded +save("historical-pid-collision", content) +print(json.dumps(files)) diff --git a/scripts/lib/release-checkpoint.mjs b/scripts/lib/release-checkpoint.mjs new file mode 100644 index 0000000000..1d1950d5fb --- /dev/null +++ b/scripts/lib/release-checkpoint.mjs @@ -0,0 +1,333 @@ +import { gt, major, valid } from "semver"; +import { z } from "zod"; + +const shaSchema = z.string().regex(/^[a-f0-9]{40}$/u); +const versionSchema = z.object({ version: z.string() }); +const manifestSchema = z.object({ ".": z.string() }); +const sectionsSchema = z.object({ + "changelog-sections": z.array( + z.object({ type: z.string(), hidden: z.boolean().optional() }), + ), +}); +const GITHUB_MERGE_SUBJECT = /^Merge pull request #(\d+) from \S+/u; + +function conventionalTitle(subject, body) { + if (!GITHUB_MERGE_SUBJECT.test(subject)) return subject; + // GitHub's default merge message puts the PR title after its subject. + // Inspect that first nonempty line, not arbitrary examples in commit prose. + return ( + body + .split(/\r?\n/u) + .slice(1) + .find((line) => line.trim() !== "") + ?.trim() ?? subject + ); +} + +function releaseVersion(value, location) { + if (valid(value) !== value || value.includes("+")) { + throw new Error( + `${location} must contain an exact SemVer without build metadata: ${value}`, + ); + } + return value; +} + +async function readJson(git, sha, path, schema) { + const text = await git(["show", `${sha}:${path}`]); + let value; + try { + value = JSON.parse(text); + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(`${path}@${sha} contains invalid JSON: ${error.message}`); + } + throw error; + } + const parsed = schema.safeParse(value); + if (!parsed.success) { + throw new Error(`${path}@${sha} is malformed: ${parsed.error.message}`); + } + return parsed.data; +} + +function readCommits(text) { + if (text.length === 0) return []; + const fields = text.split("\0"); + if (fields.pop() !== "" || fields.length % 3 !== 0) { + throw new Error("Git returned malformed checkpoint history records."); + } + const commits = []; + for (let index = 0; index < fields.length; index += 3) { + const sha = shaSchema.parse(fields[index]); + const subject = z.string().parse(fields[index + 1]); + const body = z.string().parse(fields[index + 2]); + const title = conventionalTitle(subject, body); + const type = /^([a-z]+)(?:\([^)]*\))?!?: /u.exec(title)?.[1]; + commits.push({ + sha, + subject, + ...(title !== subject ? { conventionalTitle: title } : {}), + type, + breaking: + /^[a-z]+(?:\([^)]*\))?!: /u.test(title) || + /^BREAKING(?: CHANGE|-CHANGE):\s*\S/mu.test(body), + }); + } + return commits; +} + +function referencesCommit(text, commit) { + if (text.includes(commit.sha.slice(0, 7))) return true; + const pullRequest = + GITHUB_MERGE_SUBJECT.exec(commit.subject)?.[1] ?? + /\(#(\d+)\)$/u.exec(commit.subject)?.[1]; + return ( + pullRequest !== undefined && + new RegExp(`#${pullRequest}(?!\\d)`, "u").test(text) + ); +} + +function releaseNotes(text, expectedVersion) { + const headings = Array.from(text.matchAll(/^## \[([^\]]+)\].*$/gmu)); + const first = headings[0]; + if (first?.[1] !== expectedVersion || first.index === undefined) { + throw new Error(`CHANGELOG.md must start with release ${expectedVersion}.`); + } + return text.slice(first.index, headings[1]?.index ?? text.length); +} + +function breakingSection(notes) { + const heading = /^### [^\n]*BREAKING CHANGES[^\n]*\n/imu.exec(notes); + if (heading === null) return ""; + const body = notes.slice(heading.index + heading[0].length); + const end = body.search(/^### /mu); + return end < 0 ? body : body.slice(0, end); +} + +async function requireAncestor(git, ancestor, descendant, description) { + try { + await git(["merge-base", "--is-ancestor", ancestor, descendant]); + } catch (error) { + const failure = z.object({ code: z.literal(1) }).safeParse(error); + if (!failure.success) throw error; + throw new Error( + `${description}: ${ancestor} is not an ancestor of ${descendant}. Sync the released tag into main before cutting a checkpoint, and prepare candidates from that frozen source.`, + ); + } +} + +async function resolveBaseline(git, tag) { + try { + return shaSchema.parse( + (await git(["rev-parse", "--verify", `${tag}^{commit}`])).trim(), + ); + } catch (error) { + const failure = z.object({ code: z.literal(128) }).safeParse(error); + if (!failure.success) throw error; + throw new Error( + `Published baseline ${tag} is unavailable. Fetch release tags and finish the previous release sync before preparing this checkpoint. ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +async function validateCandidate(git, sha, version, commits) { + const versions = [ + [ + "package.json", + (await readJson(git, sha, "package.json", versionSchema)).version, + ], + [ + ".release-please-manifest.json", + ( + await readJson( + git, + sha, + ".release-please-manifest.json", + manifestSchema, + ) + )["."], + ], + ]; + const lock = await readJson( + git, + sha, + "package-lock.json", + versionSchema.extend({ + packages: z.object({ "": versionSchema }), + }), + ); + versions.push( + ["package-lock.json", lock.version], + ["package-lock.json packages.root", lock.packages[""].version], + ); + const server = await readJson( + git, + sha, + "server.json", + versionSchema.extend({ + packages: z.array( + z.object({ identifier: z.string(), version: z.string() }), + ), + }), + ); + const npmPackage = server.packages.find( + (entry) => entry.identifier === "rea-agents", + ); + if (npmPackage === undefined) + throw new Error("server.json is missing the rea-agents package."); + versions.push( + ["server.json", server.version], + ["server.json rea-agents package", npmPackage.version], + ); + // Catalogs are generated and checked from this candidate by the release job; + // they are not version authority stored in the immutable source tree. + const generated = await git([ + "show", + `${sha}:src/generatedPackageMetadata.ts`, + ]); + const generatedVersions = Array.from( + generated.matchAll(/^\s+version: "([^"]+)"/gmu), + ); + if (generatedVersions.length !== 1) + throw new Error( + "Generated package metadata must declare one literal version.", + ); + versions.push(["src/generatedPackageMetadata.ts", generatedVersions[0]?.[1]]); + for (const [location, actual] of versions) { + if (actual !== version) { + throw new Error( + `${location}@${sha} declares ${actual}; release branch requires ${version}.`, + ); + } + } + const notes = releaseNotes( + await git(["show", `${sha}:CHANGELOG.md`]), + version, + ); + const breakingNotes = breakingSection(notes); + const missingBreaking = commits.filter( + (commit) => commit.breaking && !referencesCommit(breakingNotes, commit), + ); + if (missingBreaking.length > 0) { + throw new Error( + `Release ${version} migration notes omit unreleased breaking changes: ${missingBreaking.map((commit) => `${commit.sha} ${commit.subject}`).join("; ")}`, + ); + } + return commits.filter((commit) => !referencesCommit(notes, commit)); +} + +/** Validate a recorded checkpoint and optional candidate using exact Git ancestry. */ +export async function inspectReleaseCheckpoint(git, options) { + const { phase, stage, releaseBranch, sourceSha, candidateSha } = z + .object({ + phase: z.enum(["prepare", "publish"]), + stage: z.enum(["source", "candidate"]), + releaseBranch: z.string().startsWith("release/"), + sourceSha: shaSchema, + candidateSha: shaSchema.optional(), + }) + .parse(options); + const expectedVersion = releaseVersion( + releaseBranch.slice("release/".length), + "release/VERSION", + ); + if (phase === "publish" && stage === "candidate") { + throw new Error( + "Publish validates its merged source SHA; candidate stage applies only to prepare.", + ); + } + const applicationSha = + phase === "publish" + ? shaSchema.parse((await git(["rev-parse", `${sourceSha}^1`])).trim()) + : sourceSha; + const manifest = await readJson( + git, + applicationSha, + ".release-please-manifest.json", + manifestSchema, + ); + const baselineVersion = releaseVersion( + manifest["."], + "published baseline manifest", + ); + const sourcePackage = await readJson( + git, + applicationSha, + "package.json", + versionSchema, + ); + if (sourcePackage.version !== baselineVersion) { + throw new Error( + `Source package ${sourcePackage.version} and release baseline ${baselineVersion} disagree.`, + ); + } + if (!gt(expectedVersion, baselineVersion)) { + throw new Error( + `Expected release ${expectedVersion} must advance published baseline ${baselineVersion}.`, + ); + } + const baselineTag = `refs/tags/rea-agents-${baselineVersion}`; + const baselineSha = await resolveBaseline(git, baselineTag); + await requireAncestor( + git, + baselineSha, + applicationSha, + "Published baseline is outside the checkpoint ancestry", + ); + const configuration = await readJson( + git, + applicationSha, + "release-please-config.json", + sectionsSchema, + ); + const visibleTypes = new Set( + configuration["changelog-sections"] + .filter((section) => !section.hidden) + .map((section) => section.type), + ); + const commits = readCommits( + await git([ + "log", + "--first-parent", + "-z", + "--format=%H%x00%s%x00%B", + `${baselineSha}..${applicationSha}`, + ]), + ).filter((commit) => commit.breaking || visibleTypes.has(commit.type)); + const breaking = commits.filter((commit) => commit.breaking); + if ( + breaking.length > 0 && + major(baselineVersion) > 0 && + major(expectedVersion) <= major(baselineVersion) + ) { + throw new Error( + `Expected release ${expectedVersion} contains unreleased breaking markers since ${baselineVersion}: ${breaking.map((commit) => `${commit.sha} ${commit.subject}`).join("; ")}. Review compatibility or select a major version.`, + ); + } + let snapshot = phase === "publish" ? sourceSha : candidateSha; + if (stage === "candidate" && snapshot === undefined) { + snapshot = shaSchema.parse((await git(["rev-parse", "HEAD"])).trim()); + } + if (phase === "publish" || stage === "candidate") { + await requireAncestor( + git, + applicationSha, + snapshot, + "Candidate is outside the selected checkpoint ancestry", + ); + } + const missingNotes = + phase === "publish" || stage === "candidate" + ? await validateCandidate(git, snapshot, expectedVersion, commits) + : []; + return { + expectedVersion, + baselineVersion, + baselineSha, + sourceSha, + applicationSha, + commits, + missingNotes, + }; +} diff --git a/scripts/rea.mjs b/scripts/rea.mjs index 4e310bf080..1f720a4d11 100755 --- a/scripts/rea.mjs +++ b/scripts/rea.mjs @@ -19,7 +19,7 @@ const runtimeFiles = isMcpMode ? ["dist/main.js"] : isMcpDoctorMode ? ["dist/main.js", "dist/mcpDoctor.js"] - : ["dist/cli.js", "dist/cliOutput.js"]; + : ["dist/cli.js", "dist/cliOutput.js", "dist/cli/streamedJsonOutput.js"]; if (!(await compiledRuntimeExists(runtimeFiles))) { process.stderr.write( @@ -39,6 +39,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { process.exitCode = result.exitCode; } else { const { createCli } = await import("../dist/cli.js"); + const { createStreamedCliJsonOutput } = + await import("../dist/cli/streamedJsonOutput.js"); const { renderCliOutputArgumentError, renderEmptyFilteredCliOutput, @@ -51,14 +53,20 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { process.exitCode = 1; } else { let wroteOutput = false; - await createCli().serve(args, { + const resultOutput = createStreamedCliJsonOutput(args, process.stdout); + await createCli(process.env, resultOutput).serve(args, { stdout: (output) => { + if (resultOutput?.handled) { + if (resultOutput.failed) + process.stderr.write(sanitizeCliOutput(output)); + return; + } const sanitized = sanitizeCliOutput(output); if (sanitized.length > 0) wroteOutput = true; process.stdout.write(sanitized); }, }); - if (!wroteOutput) + if (!wroteOutput && !resultOutput?.handled) process.stdout.write(renderEmptyFilteredCliOutput(args) ?? ""); } } diff --git a/scripts/verify-binary-layout.mjs b/scripts/verify-binary-layout.mjs new file mode 100644 index 0000000000..280ec58f15 --- /dev/null +++ b/scripts/verify-binary-layout.mjs @@ -0,0 +1,1290 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + copyFile, + mkdir, + readFile, + readdir, + writeFile, +} from "node:fs/promises"; +import { dirname, isAbsolute, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js"; +import { parseEvidence } from "../dist/domain/evidence.js"; +import { mcpTextValue } from "./lib/mcp-verifier-results.mjs"; +import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; +import { sectionNameFixtures } from "./lib/elf-section-name-fixtures.mjs"; +import { withLargeResultMcp } from "./lib/large-result-mcp.mjs"; + +const verifyLargeMcp = process.env.REA_VERIFY_LARGE_ELF_MCP === "1"; +const python = process.env.REA_PWNTOOLS_PYTHON; +const strace = process.env.REA_VERIFY_STRACE_COMMAND; +if ( + process.platform !== "linux" || + process.arch !== "x64" || + !isAbsolute(python ?? "") || + !isAbsolute(strace ?? "") +) + throw new Error( + "verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld and strip must be available. No target is executed.", + ); +const run = createVerifierRun(); +const execute = promisify(execFile); +for (const command of ["gcc", "ld", "strip", "/usr/bin/prlimit", strace]) { + try { + await execute(command, [command === strace ? "-V" : "--version"], { + timeout: 10_000, + maxBuffer: 1024 * 1024, + }); + } catch (cause) { + throw new Error( + `verify:binary:layout prerequisite unavailable: ${command}`, + { cause }, + ); + } +} +const entrypoint = + process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url)); +const root = await PrivateRuntimeRoot.create({ + prefix: "rea-layout-verifier-", +}); +const environment = Object.fromEntries( + Object.entries(process.env).filter(([, value]) => typeof value === "string"), +); +const client = new Client({ name: "binary-layout-verifier", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: environment, + stderr: "pipe", +}); +const source = fileURLToPath( + new URL("./fixtures/binary-layout.c", import.meta.url), +); +const highSource = fileURLToPath( + new URL("./fixtures/binary-layout-high.S", import.meta.url), +); +let cases = 0; +let bootstrapCases = 0; +const failures = []; +try { + const bootstrapRoot = join(root.path, "bootstrap-boundary"); + await mkdir(bootstrapRoot); + const bootstrapPath = join(bootstrapRoot, "layout.py"); + await copyFile( + join(dirname(dirname(entrypoint)), "bridge/pwntools/layout.py"), + bootstrapPath, + ); + for (const [source, status] of [ + ["raise MemoryError('source-owned initialization failure')", 75], + ["raise OSError(12, 'source-owned allocation failure')", 75], + ["raise OSError(27, 'source-owned file write failure')", 76], + [ + "raise OSError(2, 'MemoryError-like text is not an allocation failure')", + 1, + ], + ]) { + await writeFile(join(bootstrapRoot, "layout_impl.py"), source); + const markerPath = join(bootstrapRoot, `failure-${bootstrapCases}`); + let exitCode = 0; + try { + await execute( + python, + ["-I", bootstrapPath, markerPath, "unused-request"], + { + timeout: 10000, + maxBuffer: 1024 * 1024, + }, + ); + } catch (cause) { + assert.equal(typeof cause.code, "number"); + exitCode = cause.code; + } + assert.equal(exitCode, status); + if (status === 75 || status === 76) + assert.equal( + await readFile(markerPath, "ascii"), + status === 75 ? "M" : "F", + ); + else await assert.rejects(readFile(markerPath), { code: "ENOENT" }); + bootstrapCases++; + } + for (const [name, flags] of [ + [ + "protected", + [ + "-fPIE", + "-pie", + "-fstack-protector-all", + "-Wl,-z,relro,-z,now", + "-Wl,-z,noexecstack", + ], + ], + [ + "plain", + [ + "-fno-pie", + "-no-pie", + "-fno-stack-protector", + "-Wl,-z,norelro", + "-Wl,-z,execstack", + ], + ], + ["relocatable", ["-c", "-fcommon", "-fPIC"]], + ["library", ["-shared", "-fPIC", "-fno-stack-protector"]], + ["packed-relative", ["-shared", "-fPIC", "-Wl,-z,pack-relative-relocs"]], + ]) + await execute( + "gcc", + ["-O1", "-g", ...flags, source, "-o", join(root.path, name)], + { timeout: 30_000, maxBuffer: 1024 * 1024 }, + ); + const sectionlessBytes = await readFile(join(root.path, "protected")); + sectionlessBytes.writeBigUInt64LE(0n, 40); + sectionlessBytes.writeUInt16LE(0, 60); + sectionlessBytes.writeUInt16LE(0, 62); + await writeFile(join(root.path, "sectionless"), sectionlessBytes); + const nullBytes = Buffer.from(await readFile(join(root.path, "protected"))); + const phOffset = Number(nullBytes.readBigUInt64LE(32)); + const phSize = nullBytes.readUInt16LE(54); + const phCount = nullBytes.readUInt16LE(56); + const unusedIndex = Array.from({ length: phCount }, (_, index) => index).find( + (index) => nullBytes.readUInt32LE(phOffset + index * phSize) === 0x6474e551, + ); + assert.notEqual( + unusedIndex, + undefined, + "Required stack fixture header absent", + ); + const unusedHeader = phOffset + unusedIndex * phSize; + nullBytes.writeUInt32LE(0, unusedHeader); + for (const field of [8, 32, 40]) + nullBytes.writeBigUInt64LE(0xffffffffffffffffn, unusedHeader + field); + await writeFile(join(root.path, "unused-segment"), nullBytes); + const emptyBytes = Buffer.from(nullBytes); + emptyBytes.writeUInt32LE(1, unusedHeader); + emptyBytes.writeBigUInt64LE(0n, unusedHeader + 32); + emptyBytes.writeBigUInt64LE(0n, unusedHeader + 40); + await writeFile(join(root.path, "empty-segment"), emptyBytes); + await copyFile(join(root.path, "protected"), join(root.path, "stripped")); + await execute("strip", ["--strip-all", join(root.path, "stripped")], { + timeout: 10_000, + }); + await execute("gcc", ["-c", highSource, "-o", join(root.path, "high.o")], { + timeout: 10_000, + }); + await execute( + "ld", + [ + "-Ttext=0x20000000000001", + "-o", + join(root.path, "high"), + join(root.path, "high.o"), + ], + { timeout: 10_000 }, + ); + await client.connect(transport); + const reports = new Map(); + for (const name of [ + "protected", + "plain", + "relocatable", + "library", + "packed-relative", + "stripped", + "sectionless", + "unused-segment", + "empty-segment", + "high", + ]) { + const path = join(root.path, name); + const bytes = await readFile(path); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, path); + assert.equal(value.artifact.sha256, sha256); + assert.equal(value.artifact.bytes, bytes.length); + assert.equal(value.runtime_load_base, null); + assert.equal(value.linkage.runtime_library_paths, null); + assert.equal(value.mitigations.evidence_kind, "inferred"); + if (name === "sectionless") { + assert.equal(value.sections.length, 0); + assert.equal(value.symbols.length, 0); + assert.deepEqual( + value.linkage.needed_libraries, + reports.get("protected").linkage.needed_libraries, + ); + assert.ok( + value.linkage.needed_libraries.every( + (name) => name.location !== null && name.bytes_base64 !== null, + ), + ); + // Preserve the unchanged engine's section-dependent heuristic and its + // explicit coverage limitation, rather than inventing a stronger label. + assert.equal(value.mitigations.relro, "Partial"); + assert.ok( + value.limitations.some((item) => + item.includes("RELRO/canary indicators may be incomplete"), + ), + ); + } else + assert.ok( + value.sections.some((section) => section.file_backing === "none"), + ); + for (const section of value.sections) { + assert.ok(typeof section.address === "string"); + if (section.file_backing === "file") + assert.ok( + BigInt(section.offset) + BigInt(section.size) <= + BigInt(bytes.length), + ); + } + if (name === "unused-segment" || name === "empty-segment") { + const segment = value.segments[unusedIndex]; + assert.equal(segment.file_backing, "none"); + assert.equal(segment.offset, "0xffffffffffffffff"); + if (name === "unused-segment") assert.equal(segment.permissions, null); + else assert.notEqual(segment.permissions, null); + } + assert.equal(value.relocation_inventory_completeness, "unknown"); + if (name === "library" || name === "packed-relative") { + assert.equal(value.entry_point.reported_value, "0x0"); + assert.equal(value.entry_point.meaning, "absent"); + } + if (name === "packed-relative") { + assert.ok(value.packed_relative_relocations.length > 0); + for (const table of value.packed_relative_relocations) { + assert.equal(table.evidence_kind, "derived"); + assert.equal(table.offset_meaning, "linked-virtual-address"); + assert.equal(table.entry_source_locations, null); + assert.equal(table.addends, null); + assert.ok(table.entries.length > 0); + const start = Number(BigInt(table.location.offset)); + const length = Number(BigInt(table.location.bytes)); + assert.deepEqual( + Buffer.from(table.encoded_bytes_base64, "base64"), + bytes.subarray(start, start + length), + ); + } + } + reports.set(name, value); + assert.deepEqual(await readFile(path), bytes); + cases++; + } + } + const protectedReport = reports.get("protected"); + const fileSymbols = protectedReport.symbols.filter( + (symbol) => symbol.type === "STT_FILE", + ); + assert.ok(fileSymbols.length > 0); + assert.ok( + fileSymbols.every((symbol) => symbol.value_meaning === "no-address"), + ); + const plain = reports.get("plain"); + assert.equal(protectedReport.mitigations.position_independent, true); + assert.equal(protectedReport.mitigations.nx_indicator, true); + assert.equal(protectedReport.mitigations.stack_canary_indicator, true); + assert.equal(protectedReport.mitigations.relro, "Full"); + assert.equal(plain.mitigations.position_independent, false); + assert.equal(plain.mitigations.nx_indicator, null); + assert.equal(plain.mitigations.executable_stack_indicator, true); + assert.equal(protectedReport.mitigations.executable_stack_indicator, false); + assert.equal(plain.mitigations.stack_canary_indicator, false); + assert.equal(plain.mitigations.relro, null); + const relocatable = reports.get("relocatable"); + assert.equal(relocatable.entry_point.meaning, "not-applicable"); + assert.ok(relocatable.relocations.length > 0); + assert.ok( + relocatable.relocations.every( + (item) => item.target.kind === "section-offset", + ), + ); + assert.ok( + relocatable.relocations.some((item) => BigInt(item.addend ?? "0") < 0n), + ); + assert.equal( + relocatable.symbols.find((symbol) => symbol.name.display === "common_value") + .value_meaning, + "alignment", + ); + assert.equal( + reports + .get("library") + .symbols.find((symbol) => symbol.name.display === "tls_value") + .value_meaning, + "tls-offset", + ); + assert.equal( + reports.get("high").entry_point.reported_value, + "0x20000000000001", + ); + assert.equal( + reports + .get("high") + .symbols.find((symbol) => symbol.name.display === "absolute_value").value, + "0x20000000000007", + ); + assert.equal( + reports + .get("high") + .symbols.find((symbol) => symbol.name.display === "absolute_value") + .value_meaning, + "absolute-value", + ); + assert.ok( + reports.get("stripped").symbols.length < protectedReport.symbols.length, + ); + const object = await readFile(join(root.path, "relocatable")); + const name = relocatable.symbols.find( + (symbol) => symbol.name.display === "read_values", + ).name; + const changed = Buffer.from(object); + changed[Number(BigInt(name.location.offset))] = 0xff; + const opaque = join(root.path, "opaque-name.o"); + await writeFile(opaque, changed); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, opaque); + const symbol = value.symbols.find((item) => + item.name.display.endsWith("ead_values"), + ); + assert.equal(Buffer.from(symbol.name.bytes_base64, "base64")[0], 0xff); + assert.equal(symbol.name.location.offset, name.location.offset); + assert.deepEqual(await readFile(opaque), changed); + cases++; + } + const sectionBearingBytes = await readFile(join(root.path, "protected")); + const dynamicSegment = protectedReport.segments.find( + (segment) => segment.type === "PT_DYNAMIC", + ); + const interpreterSegment = protectedReport.segments.find( + (segment) => segment.type === "PT_INTERP", + ); + assert.notEqual(dynamicSegment, undefined); + assert.notEqual(interpreterSegment, undefined); + const dependency = protectedReport.linkage.needed_libraries[0]; + assert.notEqual(dependency, undefined); + assert.notEqual(dependency.location, null); + for (const [profile, original] of [ + ["section-bearing", sectionBearingBytes], + ["sectionless", sectionlessBytes], + ]) { + for (const facet of ["dependency", "interpreter"]) { + const bytes = Buffer.from(original); + const reference = + facet === "dependency" + ? dependency + : protectedReport.linkage.interpreters[0]; + assert.notEqual(reference, undefined); + assert.notEqual(reference.location, null); + const raw = Buffer.from(reference.bytes_base64, "base64"); + const changedIndex = facet === "dependency" ? 0 : 1; + raw[changedIndex] = 0xff; + bytes[Number(BigInt(reference.location.offset)) + changedIndex] = 0xff; + const path = join(root.path, `opaque-${profile}-${facet}`); + await writeFile(path, bytes); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, path); + const actual = + facet === "dependency" + ? value.linkage.needed_libraries[0] + : value.linkage.interpreters[0]; + assert.deepEqual(actual, { + ...reference, + display: raw.toString("utf8"), + bytes_base64: raw.toString("base64"), + }); + assert.equal( + value.artifact.sha256, + createHash("sha256").update(bytes).digest("hex"), + ); + assert.deepEqual(await readFile(path), bytes); + cases++; + } + } + } + for (const fixture of sectionNameFixtures( + sectionBearingBytes, + protectedReport, + )) { + const path = join(root.path, `section-names-${fixture.name}`); + await writeFile(path, fixture.bytes); + for (const mode of ["cli", "mcp"]) { + if ( + fixture.expectation === "resolved" && + mode === "mcp" && + !verifyLargeMcp + ) + continue; + // Ordinary cases retain the SDK default. Only this complete 65,281-row + // case selects a larger receive budget; production data is not truncated. + const value = + fixture.expectation === "resolved" && mode === "mcp" + ? await withLargeResultMcp( + { entrypoint, environment, maxBufferSize: 256 * 1024 * 1024 }, + (largeClient) => + inspect(mode, path, undefined, environment, largeClient), + ) + : await inspect( + mode, + path, + fixture.expectation === "invalid_input" + ? "invalid_input" + : undefined, + ); + if (fixture.expectation === "absent") { + assert.deepEqual(value.symbols, protectedReport.symbols); + assert.deepEqual( + value.sections.map((section) => section.name), + protectedReport.sections.map(() => ({ + display: "", + bytes_base64: null, + location: null, + unknown_reason: + "ELF declares no section-name table (SHN_UNDEF index 0).", + })), + ); + assert.deepEqual( + value.sections.map((section) => section.name_offset), + protectedReport.sections.map((section) => section.name_offset), + ); + } else if (fixture.expectation === "resolved") { + assert.equal(value.sections.length, fixture.sectionCount); + assert.deepEqual( + value.sections[fixture.tableIndex].name, + protectedReport.sections[sectionBearingBytes.readUInt16LE(62)].name, + ); + assert.deepEqual(value.symbols, protectedReport.symbols); + } + cases++; + } + assert.deepEqual(await readFile(path), fixture.bytes); + } + // Malformed references must fail before the decoder reads unrelated bytes. + const neededTagOffsets = []; + let stringSizeTagOffset; + for ( + let offset = Number(BigInt(dynamicSegment.offset)); + offset < + Number(BigInt(dynamicSegment.offset) + BigInt(dynamicSegment.file_size)); + offset += 16 + ) { + const tag = sectionBearingBytes.readBigUInt64LE(offset); + if (tag === 0n) break; + if (tag === 1n) neededTagOffsets.push(offset + 8); + if (tag === 10n) stringSizeTagOffset = offset + 8; + } + assert.ok(neededTagOffsets.length > 0); + assert.notEqual(stringSizeTagOffset, undefined); + for (const [profile, original] of [ + ["section-bearing", sectionBearingBytes], + ["sectionless", sectionlessBytes], + ]) { + for (const problem of ["offset-at-end", "truncated-table"]) { + const bytes = Buffer.from(original); + if (problem === "offset-at-end") + bytes.writeBigUInt64LE( + bytes.readBigUInt64LE(stringSizeTagOffset), + neededTagOffsets[0], + ); + else { + assert.ok(bytes.readBigUInt64LE(neededTagOffsets[0]) > 0n); + bytes.writeBigUInt64LE(1n, stringSizeTagOffset); + } + const path = join(root.path, `invalid-dependency-${profile}-${problem}`); + await writeFile(path, bytes); + for (const mode of ["cli", "mcp"]) { + await inspect(mode, path, "invalid_input"); + cases++; + } + assert.deepEqual(await readFile(path), bytes); + } + } + for (const type of ["SHT_SYMTAB", "SHT_DYNSYM"]) { + const table = protectedReport.sections.find( + (section) => section.type === type, + ); + const other = protectedReport.sections.find( + (section) => section.type === "SHT_PROGBITS", + ); + assert.notEqual(table, undefined); + assert.notEqual(other, undefined); + const header = Number(BigInt(table.header_location.offset)); + const strings = protectedReport.sections[table.link]; + assert.equal(strings.type, "SHT_STRTAB"); + for (const problem of [ + "out-of-range", + "null", + "wrong-type", + "undeclared-header", + ]) { + let bytes = Buffer.from(sectionBearingBytes); + let link = + problem === "null" + ? 0 + : problem === "wrong-type" + ? other.index + : protectedReport.sections.length + 100; + if (problem === "undeclared-header") { + // Upstream can read an otherwise valid header beyond declared e_shnum. + const end = + Number(bytes.readBigUInt64LE(40)) + + protectedReport.sections.length * bytes.readUInt16LE(58); + const extended = Buffer.alloc(Math.max(bytes.length, end + 64)); + bytes.copy(extended); + const start = Number(BigInt(strings.header_location.offset)); + bytes.copy(extended, end, start, start + 64); + bytes = extended; + link = protectedReport.sections.length; + } + bytes.writeUInt32LE(link, header + 40); + const path = join(root.path, `invalid-symbol-link-${type}-${problem}`); + await writeFile(path, bytes); + for (const mode of ["cli", "mcp"]) { + await inspect(mode, path, "invalid_input"); + cases++; + } + assert.deepEqual(await readFile(path), bytes); + } + } + const shortDynamic = Buffer.from(sectionBearingBytes); + shortDynamic.writeBigUInt64LE( + 16n, + Number(BigInt(dynamicSegment.header_location.offset)) + 32, + ); + assert.notEqual( + shortDynamic.readBigUInt64LE(Number(BigInt(dynamicSegment.offset))), + 0n, + ); + const paddedInterpreter = Buffer.from(sectionBearingBytes); + const interpreterStart = Number(BigInt(interpreterSegment.offset)); + paddedInterpreter[interpreterStart + 2] = 0; + const paddedInterpreterPath = join(root.path, "padded-interpreter"); + await writeFile(paddedInterpreterPath, paddedInterpreter); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, paddedInterpreterPath); + assert.deepEqual(value.linkage.interpreters, [ + { + display: "/l", + bytes_base64: Buffer.from("/l").toString("base64"), + location: { offset: interpreterSegment.offset, bytes: "0x3" }, + unknown_reason: null, + }, + ]); + assert.equal( + value.segments[interpreterSegment.index].file_size, + interpreterSegment.file_size, + ); + assert.deepEqual(await readFile(paddedInterpreterPath), paddedInterpreter); + cases++; + } + const unterminatedInterpreter = Buffer.from(sectionBearingBytes); + unterminatedInterpreter.fill( + 0x58, + interpreterStart, + interpreterStart + Number(BigInt(interpreterSegment.file_size)), + ); + const zeroSymbolEntries = ["SHT_SYMTAB", "SHT_DYNSYM"].map((type) => { + const section = protectedReport.sections.find((item) => item.type === type); + assert.notEqual( + section, + undefined, + `Required fixture table absent: ${type}`, + ); + const bytes = Buffer.from(sectionBearingBytes); + bytes.writeBigUInt64LE( + 0n, + Number(BigInt(section.header_location.offset)) + 56, + ); + return [`zero-entry-${type}`, bytes, "invalid_input"]; + }); + const undersizedSymbolEntries = ["SHT_SYMTAB", "SHT_DYNSYM"].map((type) => { + const section = protectedReport.sections.find((item) => item.type === type); + assert.notEqual(section, undefined); + const bytes = Buffer.from(sectionBearingBytes); + const header = Number(BigInt(section.header_location.offset)); + bytes.writeBigUInt64LE(8n, header + 32); + bytes.writeBigUInt64LE(8n, header + 56); + return [`undersized-entry-${type}`, bytes, "invalid_input"]; + }); + const relocationSection = relocatable.sections.find( + (item) => item.type === "SHT_RELA", + ); + assert.notEqual(relocationSection, undefined); + const undersizedRelocationEntries = [ + ["REL", 9], + ["RELA", 4], + ].map(([type, sectionType]) => { + const bytes = Buffer.from(object); + const header = Number(BigInt(relocationSection.header_location.offset)); + bytes.writeUInt32LE(sectionType, header + 4); + bytes.writeBigUInt64LE(8n, header + 32); + bytes.writeBigUInt64LE(8n, header + 56); + return [`undersized-entry-${type}`, bytes, "invalid_input"]; + }); + const invalidRelocationReferences = [ + ["missing-symbol-table", relocatable.sections.length + 100], + ["wrong-symbol-table-kind", 1], + ["positive-symbol-without-table", 0], + ["missing-relocation-symbol", null], + ].map(([name, link]) => { + const bytes = Buffer.from(object); + const header = Number(BigInt(relocationSection.header_location.offset)); + if (link === null) { + const offset = Number(BigInt(relocationSection.offset)) + 8; + const type = bytes.readBigUInt64LE(offset) & 0xffffffffn; + bytes.writeBigUInt64LE((0xffffffffn << 32n) | type, offset); + } else bytes.writeUInt32LE(link, header + 40); + return [name, bytes, "invalid_input"]; + }); + const inactiveTarget = relocatable.sections.find( + (section) => section.name.display === ".comment", + ); + assert.notEqual( + inactiveTarget, + undefined, + "Required inactive-target fixture absent", + ); + for (const [name, targetIndex, reason] of [ + ["undefined-relocation-target", 0, "undefined-section-reference"], + [ + "inactive-relocation-target", + inactiveTarget.index, + "inactive-section-header", + ], + ]) { + const bytes = Buffer.from(object); + bytes.writeUInt32LE( + targetIndex, + Number(BigInt(relocationSection.header_location.offset)) + 44, + ); + if (targetIndex !== 0) + bytes.writeUInt32LE( + 0, + Number(BigInt(inactiveTarget.header_location.offset)) + 4, + ); + const inactiveSymbol = relocatable.symbols.find( + (symbol) => symbol.name.display === "read_values", + ); + assert.notEqual(inactiveSymbol, undefined); + if (targetIndex !== 0) + bytes.writeUInt16LE( + targetIndex, + Number(BigInt(inactiveSymbol.location.offset)) + 6, + ); + const path = join(root.path, name); + await writeFile(path, bytes); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, path); + const rows = value.relocations.filter( + (item) => item.section_index === relocationSection.index, + ); + assert.ok(rows.length > 0); + for (const row of rows) + assert.deepEqual(row.target, { + kind: "unknown-section", + reported_section_index: targetIndex, + offset: row.reported_offset, + unknown_reason: reason, + }); + if (targetIndex !== 0) + assert.equal( + value.symbols.find((symbol) => symbol.name.display === "read_values") + .value_meaning, + "unknown-section-index", + ); + assert.deepEqual(await readFile(path), bytes); + cases++; + } + } + const relative = protectedReport.relocations.find( + (item) => item.type === 8 && item.symbol_index === 0, + ); + assert.notEqual(relative, undefined, "Required relative relocation absent"); + const relativeOwner = protectedReport.sections[relative.section_index]; + const noSymbolTable = Buffer.from(sectionBearingBytes); + const relativeHeader = Number(BigInt(relativeOwner.header_location.offset)); + noSymbolTable.writeBigUInt64LE( + BigInt(relative.location.offset), + relativeHeader + 24, + ); + noSymbolTable.writeBigUInt64LE( + BigInt(relative.location.bytes), + relativeHeader + 32, + ); + noSymbolTable.writeUInt32LE(0, relativeHeader + 40); + const zeroSymbolPath = join(root.path, "relative-without-symbol-table"); + await writeFile(zeroSymbolPath, noSymbolTable); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, zeroSymbolPath); + const reported = value.relocations.find( + (item) => item.section_index === relative.section_index, + ); + assert.equal(reported.symbol_table_index, 0); + assert.equal(reported.symbol_index, 0); + assert.equal(reported.symbol_reference_meaning, "zero-symbol-value"); + assert.deepEqual(await readFile(zeroSymbolPath), noSymbolTable); + cases++; + } + const undersizedProgramHeader = Buffer.from(sectionBearingBytes); + const missingTargetSection = Buffer.from(object); + missingTargetSection.writeUInt32LE( + relocatable.sections.length + 100, + Number(BigInt(relocationSection.header_location.offset)) + 44, + ); + undersizedProgramHeader.writeUInt16LE(8, 54); + undersizedProgramHeader.writeUInt16LE(1, 56); + const symbolTable = relocatable.sections.find( + (section) => section.type === "SHT_SYMTAB", + ); + assert.notEqual(symbolTable, undefined); + const symbol = relocatable.symbols.find( + (item) => item.name.display === "read_values", + ); + assert.notEqual(symbol, undefined); + const extendedWords = Buffer.alloc( + Number(BigInt(symbolTable.size) / BigInt(symbolTable.entry_size)) * 4, + ); + extendedWords.writeUInt32LE(symbol.section_index, symbol.entry_index * 4); + const headers = Buffer.from( + object.subarray( + Number(BigInt(relocatable.sections[0].header_location.offset)), + Number( + BigInt(relocatable.sections.at(-1).header_location.offset) + + BigInt(relocatable.sections.at(-1).header_location.bytes), + ), + ), + ); + const extendedHeader = Buffer.alloc(64); + extendedHeader.writeUInt32LE(18, 4); // SHT_SYMTAB_SHNDX + extendedHeader.writeBigUInt64LE(BigInt(object.length), 24); + extendedHeader.writeBigUInt64LE(BigInt(extendedWords.length), 32); + extendedHeader.writeUInt32LE(symbolTable.index, 40); + extendedHeader.writeBigUInt64LE(4n, 48); + extendedHeader.writeBigUInt64LE(4n, 56); + const extendedObject = Buffer.concat([ + object, + extendedWords, + headers, + extendedHeader, + ]); + extendedObject.writeBigUInt64LE( + BigInt(object.length + extendedWords.length), + 40, + ); + extendedObject.writeUInt16LE(relocatable.sections.length + 1, 60); + extendedObject.writeUInt16LE( + 0xffff, + Number(BigInt(symbol.location.offset)) + 6, + ); + const extendedPath = join(root.path, "external-symbol-index.o"); + await writeFile(extendedPath, extendedObject); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, extendedPath); + const reported = value.symbols.find( + (item) => item.name.display === "read_values", + ); + assert.equal(reported.section_index, 0xffff); + assert.equal(reported.value_meaning, "unknown-section-index"); + assert.ok( + value.sections.some((section) => section.type === "SHT_SYMTAB_SHNDX"), + ); + assert.deepEqual(await readFile(extendedPath), extendedObject); + cases++; + } + for (const [name, option, expected] of [ + ["address-space", "--as=2147483648", '"address_space_bytes": 2147483648'], + ["cpu", "--cpu=20", '"cpu_seconds": 20'], + ["file-size", "--fsize=33554432", '"file_size_bytes": 33554432'], + ]) { + const wrapper = join(root.path, `python-limit-${name}`); + const quotedPython = "'" + python.replaceAll("'", "'\"'\"'") + "'"; + await writeFile( + wrapper, + `#!/bin/sh\nexec /usr/bin/prlimit ${option} -- ${quotedPython} "$@"\n`, + { mode: 0o700 }, + ); + const limitedEnvironment = { ...environment, REA_PWNTOOLS_PYTHON: wrapper }; + const limitedClient = new Client({ + name: "limited-layout-verifier", + version: "1", + }); + const limitedTransport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: limitedEnvironment, + stderr: "pipe", + }); + try { + await limitedClient.connect(limitedTransport); + for (const mode of ["cli", "mcp"]) { + const value = await inspect( + mode, + join(root.path, "protected"), + undefined, + limitedEnvironment, + limitedClient, + ); + assert.ok(value.limitations.some((item) => item.includes(expected))); + cases++; + } + } finally { + try { + await limitedClient.close(); + } finally { + await limitedTransport.close(); + } + } + } + const unsupported = Buffer.from(object); + const originalSectionOffset = Number(object.readBigUInt64LE(40)); + const sectionSize = object.readUInt16LE(58); + const originalSectionCount = object.readUInt16LE(60); + const sectionCount = 30000; + const sectionOffset = Math.ceil(object.length / 8) * 8; + const sectionHeavy = Buffer.alloc(sectionOffset + sectionCount * sectionSize); + object.copy(sectionHeavy); + object.copy( + sectionHeavy, + sectionOffset, + originalSectionOffset, + originalSectionOffset + originalSectionCount * sectionSize, + ); + for (let index = originalSectionCount; index < sectionCount; index++) { + sectionHeavy.writeUInt32LE(1, sectionOffset + index * sectionSize + 4); + sectionHeavy.writeBigUInt64LE(1n, sectionOffset + index * sectionSize + 48); + } + sectionHeavy.writeBigUInt64LE(BigInt(sectionOffset), 40); + sectionHeavy.writeUInt16LE(sectionCount, 60); + const sectionHeavyPath = join(root.path, "section-heavy.o"); + await writeFile(sectionHeavyPath, sectionHeavy); + const memoryWrapper = join(root.path, "python-memory-constraint"); + const quotedPython = "'" + python.replaceAll("'", "'\"'\"'") + "'"; + await writeFile( + memoryWrapper, + `#!/bin/sh\nexec /usr/bin/prlimit --as=100663296 -- ${quotedPython} "$@"\n`, + { mode: 0o700 }, + ); + const memoryEnvironment = { + ...environment, + REA_PWNTOOLS_PYTHON: memoryWrapper, + }; + const memoryClient = new Client({ + name: "memory-layout-verifier", + version: "1", + }); + const memoryTransport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: memoryEnvironment, + stderr: "pipe", + }); + try { + await memoryClient.connect(memoryTransport); + for (const mode of ["cli", "mcp"]) { + const error = await inspect( + mode, + sectionHeavyPath, + "resource_constraint", + memoryEnvironment, + memoryClient, + ); + assert.equal(error.code, "resource_constraint"); + assert.equal(error.details.resource, "memory"); + assert.ok( + error.details.reported_limits === null || + error.details.reported_limits.address_space_bytes === 100663296, + ); + assert.equal(error.details.captured_output.truncated, false); + assert.ok(error.remediation.action.includes("memory")); + cases++; + } + assert.deepEqual(await readFile(sectionHeavyPath), sectionHeavy); + } finally { + try { + await memoryClient.close(); + } finally { + await memoryTransport.close(); + } + } + // Extended section numbering keeps this valid while providing enough work + // to reach the one-second CPU limit even on faster CI runners. + const cpuSectionCount = 300000; + const cpuHeavy = Buffer.alloc(sectionOffset + cpuSectionCount * sectionSize); + sectionHeavy.copy(cpuHeavy); + for (let index = sectionCount; index < cpuSectionCount; index++) { + cpuHeavy.writeUInt32LE(1, sectionOffset + index * sectionSize + 4); + cpuHeavy.writeBigUInt64LE(1n, sectionOffset + index * sectionSize + 48); + } + cpuHeavy.writeUInt16LE(0, 60); + cpuHeavy.writeBigUInt64LE(BigInt(cpuSectionCount), sectionOffset + 32); + const cpuHeavyPath = join(root.path, "cpu-section-heavy.o"); + await writeFile(cpuHeavyPath, cpuHeavy); + const cpuWrapper = join(root.path, "python-cpu-constraint"); + await writeFile( + cpuWrapper, + `#!/bin/sh\nexec /usr/bin/prlimit --cpu=1: --core=0: -- ${quotedPython} "$@"\n`, + { mode: 0o700 }, + ); + const cpuEnvironment = { ...environment, REA_PWNTOOLS_PYTHON: cpuWrapper }; + const cpuClient = new Client({ name: "cpu-layout-verifier", version: "1" }); + const cpuTransport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: cpuEnvironment, + stderr: "pipe", + }); + try { + await cpuClient.connect(cpuTransport); + for (const mode of ["cli", "mcp"]) { + const error = await inspect( + mode, + cpuHeavyPath, + "resource_constraint", + cpuEnvironment, + cpuClient, + ); + assert.equal(error.details.resource, "cpu"); + assert.equal(error.details.reported_limits.cpu_seconds, 1); + assert.ok(JSON.stringify(error).includes("SIGXCPU")); + assert.ok(error.remediation.action.includes("CPU")); + cases++; + } + assert.deepEqual(await readFile(cpuHeavyPath), cpuHeavy); + } finally { + try { + await cpuClient.close(); + } finally { + await cpuTransport.close(); + } + } + for (const limit of [1024, 1]) { + const wrapper = join(root.path, `python-file-size-${limit}`); + await writeFile( + wrapper, + `#!/bin/sh\nexec /usr/bin/prlimit --fsize=${limit}: --core=0: -- ${quotedPython} "$@"\n`, + { mode: 0o700 }, + ); + const fileEnvironment = { ...environment, REA_PWNTOOLS_PYTHON: wrapper }; + const fileClient = new Client({ + name: "file-size-layout-verifier", + version: "1", + }); + const fileTransport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: fileEnvironment, + stderr: "pipe", + }); + try { + await fileClient.connect(fileTransport); + for (const mode of ["cli", "mcp"]) { + const error = await inspect( + mode, + sectionHeavyPath, + "resource_constraint", + fileEnvironment, + fileClient, + ); + assert.equal(error.details.resource, "file-size"); + if (limit === 1024) + assert.equal(error.details.reported_limits.file_size_bytes, limit); + else { + assert.equal(error.details.reported_limits, null); + assert.ok(JSON.stringify(error).includes("limit report unavailable")); + } + assert.ok(error.remediation.action.includes("file-size")); + cases++; + } + assert.deepEqual(await readFile(sectionHeavyPath), sectionHeavy); + } finally { + try { + await fileClient.close(); + } finally { + await fileTransport.close(); + } + } + } + unsupported.writeUInt16LE(183, 18); + const core = Buffer.from(object); + core.writeUInt16LE(4, 16); + for (const [name, bytes, category] of [ + ["not-elf", Buffer.from("ordinary local text"), "invalid_input"], + ["truncated", object.subarray(0, 32), "invalid_input"], + [ + "truncated-tables", + object.subarray(0, object.length - 1), + "invalid_input", + ], + ...zeroSymbolEntries, + ...undersizedSymbolEntries, + ...undersizedRelocationEntries, + ...invalidRelocationReferences, + ["unterminated-dynamic-segment", shortDynamic, "invalid_input"], + ["unterminated-interpreter", unterminatedInterpreter, "invalid_input"], + [ + "missing-relocation-target-section", + missingTargetSection, + "invalid_input", + ], + ["undersized-program-header", undersizedProgramHeader, "invalid_input"], + ["arm64", unsupported, "unsupported_target"], + ["core", core, "unsupported_target"], + ]) { + const path = join(root.path, name); + await writeFile(path, bytes); + for (const mode of ["cli", "mcp"]) { + await inspect(mode, path, category); + assert.deepEqual(await readFile(path), bytes); + cases++; + } + } + const packedFile = await readFile(join(root.path, "packed-relative")); + const packedTable = + reports.get("packed-relative").packed_relative_relocations[0]; + assert.notEqual(packedTable, undefined); + packedFile.writeBigUInt64LE(1n, Number(BigInt(packedTable.location.offset))); + const badPackedPath = join(root.path, "relr-bitmap-without-anchor"); + await writeFile(badPackedPath, packedFile); + for (const mode of ["cli", "mcp"]) { + await inspect(mode, badPackedPath, "invalid_input"); + assert.deepEqual(await readFile(badPackedPath), packedFile); + cases++; + } + const brokenPython = join(root.path, "broken-configured-python"); + await writeFile( + brokenPython, + "#!/rea-missing-configured-python-interpreter\n", + { mode: 0o700 }, + ); + const unmarkedLaunchers = []; + for (const status of [75, 76]) { + const path = join(root.path, `unmarked-launcher-${status}`); + await writeFile( + path, + `#!/bin/sh\nprintf 'launcher exited ${status}\\n' >&2\nexit ${status}\n`, + { mode: 0o700 }, + ); + unmarkedLaunchers.push(path); + } + for (const [configuredPython, category] of [ + [root.path, "unavailable"], + [brokenPython, "unavailable"], + ...unmarkedLaunchers.map((path) => [path, "execution_failure"]), + ]) { + const selectedEnvironment = { + ...environment, + REA_PWNTOOLS_PYTHON: configuredPython, + }; + const selectedClient = new Client({ + name: "unavailable-layout-verifier", + version: "1", + }); + const selectedTransport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: selectedEnvironment, + stderr: "pipe", + }); + try { + await selectedClient.connect(selectedTransport); + for (const mode of ["cli", "mcp"]) { + const error = await inspect( + mode, + join(root.path, "protected"), + category, + selectedEnvironment, + selectedClient, + ); + if (category === "unavailable") { + assert.equal(error.code, "provider_unavailable"); + assert.equal( + error.details.rejections[0].diagnostics.executable_path, + configuredPython, + ); + } else { + assert.equal(error.code, "execution_failure"); + assert.ok(JSON.stringify(error).includes("launcher exited")); + assert.ok(JSON.stringify(error).includes("resource_failure_marker")); + } + cases++; + } + } finally { + try { + await selectedClient.close(); + } finally { + await selectedTransport.close(); + } + } + } + for (const mode of ["cli", "mcp"]) { + await inspect(mode, join(root.path, "absent"), "invalid_input"); + cases++; + } + for (const name of ["protected", "sectionless"]) { + const trace = join(root.path, `exec.trace.${name}`); + await execute( + strace, + [ + "-ff", + "-e", + "trace=execve,execveat", + "-s", + "4096", + "-o", + trace, + process.execPath, + entrypoint, + "inspect-binary-layout", + join(root.path, name), + "--json", + ], + { + env: { ...environment, PATH: "/usr/bin:/bin" }, + timeout: 45_000, + maxBuffer: 256 * 1024 * 1024, + }, + ); + } + const executions = []; + for (const file of await readdir(root.path)) { + if (!file.startsWith("exec.trace.")) continue; + for (const line of (await readFile(join(root.path, file), "utf8")).split( + "\n", + )) { + if (!/execve(?:at)?\(/.test(line)) continue; + const match = /^execve\("([^"]+)"/.exec(line); + assert.notEqual( + match, + null, + `Unresolved executable identity in syscall trace: ${line}`, + ); + assert.ok( + line.endsWith(" = 0") || / = -1 [A-Z]+/.test(line), + `Incomplete exec observation: ${line}`, + ); + const ownershipInspection = + ["/usr/bin/ps", "/bin/ps"].includes(match[1]) && + line.includes('["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]'); + assert.ok( + [process.execPath, python].includes(match[1]) || ownershipInspection, + `Unexpected attempted host execution: ${line}`, + ); + executions.push(match[1]); + } + } + assert.ok(executions.includes(process.execPath)); + assert.ok(executions.includes(python)); + cases += 2; +} catch (cause) { + failures.push(cause); +} finally { + for (const close of [ + () => client.close(), + () => transport.close(), + () => root.close(), + ]) { + try { + await close(); + } catch (cause) { + failures.push(cause); + } + } +} +const verifier = await completeVerifierRun(run); +try { + assert.equal(verifier.process_lineage.status, "verified"); + assert.deepEqual(verifier.process_lineage.descendants, []); +} catch (cause) { + failures.push(cause); +} +if (failures.length > 0) { + console.error( + JSON.stringify( + { status: "failed", public_cases: cases, verifier }, + null, + 2, + ), + ); + throw new AggregateError( + failures, + "Offline binary layout verification failed; cleanup failures are retained.", + ); +} +console.log( + JSON.stringify( + { + status: "passed", + public_cases: cases, + bootstrap_boundary_cases: bootstrapCases, + large_index_mcp: verifyLargeMcp ? "verified" : "not-requested", + profile: "pwntools4.15.0/pyelftools0.33/Unicorn2.1.2", + target_execution: "exec-syscalls-verified-absent", + verifier, + }, + null, + 2, + ), +); + +async function inspect( + mode, + path, + category, + selectedEnvironment = environment, + selectedClient = client, +) { + let envelope; + if (mode === "mcp") { + const response = await selectedClient.callTool( + { + name: "inspect_binary_layout", + arguments: { path }, + }, + { timeout: 300_000 }, + ); + const value = JSON.parse(mcpTextValue(response)); + if (category !== undefined) { + assert.equal(response.isError, true); + assert.equal(value.error.category, category, JSON.stringify(value.error)); + return value.error; + } + assert.notEqual(response.isError, true, mcpTextValue(response)); + envelope = value.evidence; + assert.deepEqual(value.result, envelope.normalized_result); + } else { + let response; + try { + response = await execute( + process.execPath, + [entrypoint, "inspect-binary-layout", path, "--json"], + { + env: selectedEnvironment, + timeout: 40_000, + maxBuffer: 256 * 1024 * 1024, + }, + ); + } catch (cause) { + if (category === undefined) throw cause; + if (typeof cause.code !== "number") throw cause; + const error = JSON.parse(cause.stdout); + assert.equal(error.category, category, JSON.stringify(error)); + return error; + } + assert.equal(category, undefined, "Expected selected input to fail"); + envelope = JSON.parse(response.stdout); + } + const evidence = parseEvidence(envelope); + assert.equal(evidence.subject.local_path, path); + assert.equal( + evidence.provider.version, + "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2", + ); + assert.deepEqual(evidence.normalized_result, evidence.raw_result); + assert.equal(evidence.normalized_result.diagnostics.truncated, false); + return evidence.normalized_result; +} diff --git a/scripts/verify-evm-interface.mjs b/scripts/verify-evm-interface.mjs new file mode 100644 index 0000000000..88c37b649a --- /dev/null +++ b/scripts/verify-evm-interface.mjs @@ -0,0 +1,579 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { access, readFile, readdir, writeFile } from "node:fs/promises"; +import { constants } from "node:fs"; +import { isAbsolute, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js"; +import { parseEvidence } from "../dist/domain/evidence.js"; +import { mcpTextValue } from "./lib/mcp-verifier-results.mjs"; +import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; + +const solcModule = process.env.REA_VERIFY_SOLC_MODULE; +const strace = process.env.REA_VERIFY_STRACE_COMMAND; +if ( + process.platform !== "linux" || + process.arch !== "x64" || + !isAbsolute(solcModule ?? "") || + !isAbsolute(strace ?? "") +) + throw new Error( + "verify:evm:interface requires Linux x64, caller-supplied util-linux prlimit and absolute REA_VERIFY_SOLC_MODULE pointing to unchanged solc 0.8.30 and absolute REA_VERIFY_STRACE_COMMAND. No target or chain is executed.", + ); +try { + await access( + process.env.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit", + constants.X_OK, + ); + await access(solcModule, constants.R_OK); + await access(strace, constants.X_OK); +} catch (cause) { + throw new Error( + "verify:evm:interface prerequisite unavailable: selected solc module, strace or util-linux prlimit", + { cause }, + ); +} +const solc = (await import(pathToFileURL(solcModule).href)).default; +assert.equal(solc.version(), "0.8.30+commit.73712a01.Emscripten.clang"); +const source = await readFile( + new URL("./fixtures/evm-interface.sol", import.meta.url), + "utf8", +); +const entrypoint = + process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url)); +const execute = promisify(execFile); +const environment = Object.fromEntries( + Object.entries(process.env).filter(([, value]) => typeof value === "string"), +); +const run = createVerifierRun(); +const root = await PrivateRuntimeRoot.create({ prefix: "rea-evm-verifier-" }); +const client = new Client({ name: "evm-interface-verifier", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: environment, + stderr: "pipe", +}); +const failures = []; +let cases = 0; +try { + await client.connect(transport); + const tools = await client.listTools(); + assert.ok(tools.tools.some(({ name }) => name === "inspect_evm_interface")); + const inspect = async ( + mode, + path, + encoding, + expectedCategory, + scope = { command: process.execPath, prefix: [], client }, + ) => { + let value; + if (mode === "cli") { + let stdout; + try { + ({ stdout } = await execute( + scope.command, + [ + ...scope.prefix, + entrypoint, + "inspect-evm-interface", + path, + encoding, + "--json", + ], + { + env: scope.environment ?? environment, + timeout: 45_000, + maxBuffer: 32 * 1024 * 1024, + }, + )); + } catch (cause) { + if (typeof cause.code !== "number" || typeof cause.stdout !== "string") + throw cause; + stdout = cause.stdout; + } + value = JSON.parse(stdout); + } else { + const reply = await scope.client.callTool({ + name: "inspect_evm_interface", + arguments: { path, encoding }, + }); + value = JSON.parse(mcpTextValue(reply)); + if (expectedCategory !== undefined) { + assert.equal(reply.isError, true); + value = value.error; + } else assert.notEqual(reply.isError, true, mcpTextValue(reply)); + } + if (expectedCategory !== undefined) { + assert.equal(value.category, expectedCategory, JSON.stringify(value)); + return value; + } + assert.equal( + value.category, + undefined, + `Unexpected public failure: ${JSON.stringify(value)}`, + ); + const evidence = parseEvidence(mode === "cli" ? value : value.evidence); + const report = evidence.normalized_result; + assert.equal(report.diagnostics.truncated, false); + assert.equal(evidence.provider.version, "evmole@0.9.3"); + assert.equal(evidence.confidence, "inferred"); + assert.equal(report.artifact.path, path); + assert.equal(report.artifact.encoding, encoding); + const carrier = await readFile(path); + assert.equal( + report.artifact.sha256, + createHash("sha256").update(carrier).digest("hex"), + ); + assert.equal(evidence.subject.digest.sha256, report.artifact.sha256); + assert.equal(report.artifact.bytes, carrier.length); + assert.equal( + report.bytecode.sha256, + createHash("sha256") + .update(Buffer.from(report.bytecode.hex, "hex")) + .digest("hex"), + ); + assert.equal(report.bytecode.bytes, report.bytecode.hex.length / 2); + for (const key of ["kind", "hardfork", "deployment_authenticity"]) + assert.equal(report.bytecode[key], "unknown"); + assert.equal(report.discovery_completeness, "unknown"); + assert.equal(report.runtime_execution, "not-performed"); + assert.deepEqual( + report.functions.map((fn) => ({ + selector: fn.selector.slice(2), + bytecodeOffset: fn.bytecode_offset, + dispatch: fn.dispatch, + ...(fn.inferred_arguments === null + ? {} + : { arguments: fn.inferred_arguments }), + ...(fn.inferred_state_mutability === null + ? {} + : { stateMutability: fn.inferred_state_mutability }), + })), + evidence.raw_result.functions ?? [], + ); + if (mode === "mcp") assert.deepEqual(value.result, report); + return report; + }; + for (const [name, optimizer, viaIR] of [ + ["plain", false, false], + ["optimized", true, false], + ["via-ir", true, true], + ]) { + const output = JSON.parse( + solc.compile( + JSON.stringify({ + language: "Solidity", + sources: { "Interface.sol": { content: source } }, + settings: { + optimizer: { enabled: optimizer, runs: 200 }, + viaIR, + evmVersion: "cancun", + outputSelection: { + "*": { + "*": [ + "evm.deployedBytecode.object", + "evm.methodIdentifiers", + "abi", + ], + }, + }, + }, + }), + ), + ); + assert.ok( + !output.errors?.some(({ severity }) => severity === "error"), + JSON.stringify(output.errors), + ); + const artifact = output.contracts["Interface.sol"].InterfaceFixture; + const hex = artifact.evm.deployedBytecode.object; + const expected = Object.values(artifact.evm.methodIdentifiers) + .map((selector) => "0x" + selector) + .sort(); + const forms = [ + ["hex", ` \t0x${hex.toUpperCase()}\r\n`], + ["raw", Buffer.from(hex, "hex")], + ]; + for (const [encoding, data] of forms) { + const path = join(root.path, `${name}.${encoding}`); + await writeFile(path, data); + const original = await readFile(path); + for (const mode of ["cli", "mcp"]) { + const report = await inspect(mode, path, encoding); + assert.equal(report.bytecode.hex, hex); + assert.deepEqual( + report.functions + .filter(({ dispatch }) => dispatch === "abi") + .map(({ selector }) => selector) + .sort(), + expected, + ); + assert.ok( + report.functions.every( + ({ bytecode_offset }) => bytecode_offset < report.bytecode.bytes, + ), + ); + const set = report.functions.find( + ({ selector }) => + selector === "0x" + artifact.evm.methodIdentifiers["set(uint256)"], + ); + assert.equal(set.inferred_arguments, "uint256"); + assert.equal(set.inferred_state_mutability, "nonpayable"); + cases++; + } + assert.deepEqual(await readFile(path), original); + } + } + for (const [name, data, encoding] of [ + ["empty", "0x", "hex"], + ["empty-raw", Buffer.alloc(0), "raw"], + ["incomplete-push", Buffer.from([0x7f]), "raw"], + ["no-dispatch", "60006000f3", "hex"], + ]) { + const path = join(root.path, name); + await writeFile(path, data); + for (const mode of ["cli", "mcp"]) { + const report = await inspect(mode, path, encoding); + assert.equal(report.functions.length, 0); + cases++; + } + } + for (const [name, hex, expected] of [ + ["zero-selector", "60003560e01c630000000014601057005b00", ["0x00000000"]], + ["push-immediate", "63123456785060006000f3", []], + ]) { + const path = join(root.path, name); + await writeFile(path, hex); + for (const mode of ["cli", "mcp"]) { + const report = await inspect(mode, path, "hex"); + assert.deepEqual( + report.functions.map(({ selector }) => selector), + expected, + ); + cases++; + } + } + for (const [encoding, data] of [ + ["raw", Buffer.from("ef0001", "hex")], + ["hex", "0xef0001"], + ]) { + const path = join(root.path, "eof." + encoding); + await writeFile(path, data); + for (const mode of ["cli", "mcp"]) { + const failure = await inspect(mode, path, encoding, "unsupported_target"); + assert.equal(failure.code, "unsupported_target"); + assert.equal(failure.details.path, path); + assert.match(failure.details.reason, /EF00/); + assert.match(failure.remediation.action, /target format/); + cases++; + } + } + for (const [name, data] of [ + ["odd", "0x1"], + ["invalid-hex", "xyz"], + ["interior-space", "60 00"], + ["bom", "\uFEFF6000"], + ["utf8", Buffer.from([0xff])], + ]) { + const path = join(root.path, name); + await writeFile(path, data); + for (const mode of ["cli", "mcp"]) { + await inspect(mode, path, "hex", "invalid_input"); + cases++; + } + } + for (const mode of ["cli", "mcp"]) { + await inspect(mode, join(root.path, "absent"), "raw", "invalid_input"); + cases++; + } + for (const [option, expected] of [ + ["--as=2147483648", "address_space_bytes=2147483648"], + ["--cpu=20", "cpu_seconds=20"], + ["--fsize=8388608", "file_size_bytes=8388608"], + ]) { + const limiter = environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit"; + const prefix = [ + option, + "--", + process.execPath, + "--disable-wasm-trap-handler", + ]; + const limitedClient = new Client({ + name: "limited-evm-verifier", + version: "1", + }); + const limitedTransport = new StdioClientTransport({ + command: limiter, + args: [...prefix, entrypoint, "mcp"], + env: environment, + stderr: "pipe", + }); + try { + await limitedClient.connect(limitedTransport); + const scope = { command: limiter, prefix, client: limitedClient }; + for (const mode of ["cli", "mcp"]) { + const report = await inspect( + mode, + join(root.path, "zero-selector"), + "hex", + undefined, + scope, + ); + assert.ok(report.limitations.some((item) => item.includes(expected))); + cases++; + } + } finally { + try { + await limitedClient.close(); + } finally { + await limitedTransport.close(); + } + } + } + const brokenLimiter = join(root.path, "broken-limiter"); + await writeFile(brokenLimiter, "#!/rea-verifier-missing-interpreter\n", { + mode: 0o700, + }); + const signalLimiter = join(root.path, "signal-limiter"); + await writeFile(signalLimiter, '#!/bin/sh\nulimit -c 0\nkill -XCPU "$$"\n', { + mode: 0o700, + }); + const fileLimiter = join(root.path, "file-size-limiter"); + const unmarkedLimiter = join(root.path, "unmarked-file-size-limiter"); + await writeFile( + unmarkedLimiter, + '#!/bin/sh\nprintf "launcher exit 76\\n" >&2\nexit 76\n', + { mode: 0o700 }, + ); + // Fixture-only launcher: run the actual Node/WASM worker with a one-byte + // file allowance, keeping the usual address-space, CPU and core budgets. + await writeFile( + fileLimiter, + '#!/bin/sh\nwhile [ "$#" -gt 0 ] && [ "$1" != "--" ]; do shift; done\n[ "$#" -gt 0 ] || exit 64\nshift\nexec /usr/bin/prlimit --as=3221225472: --cpu=30: --fsize=1: --core=0: -- "$@"\n', + { mode: 0o700 }, + ); + for (const [limiter, category, resource] of [ + [root.path, "unsupported_provider"], + [brokenLimiter, "unsupported_provider"], + [signalLimiter, "resource_constraint", "cpu"], + [fileLimiter, "resource_constraint", "file-size"], + [unmarkedLimiter, "execution_failure"], + ]) { + const selectedEnvironment = { + ...environment, + REA_EVM_PRLIMIT_COMMAND: limiter, + }; + const selectedClient = new Client({ + name: "unavailable-evm-limiter-verifier", + version: "1", + }); + const selectedTransport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: selectedEnvironment, + stderr: "pipe", + }); + try { + await selectedClient.connect(selectedTransport); + const scope = { + command: process.execPath, + prefix: [], + client: selectedClient, + environment: selectedEnvironment, + }; + for (const mode of ["cli", "mcp"]) { + const error = await inspect( + mode, + join(root.path, "zero-selector"), + "hex", + category, + scope, + ); + if (category === "resource_constraint") { + assert.equal(error.code, "resource_constraint"); + assert.equal(error.details.resource, resource); + assert.equal( + error.details.reported_limits.effective_soft_limits, + null, + ); + assert.equal( + typeof error.details.reported_limits.configured_soft_limits + .cpu_seconds, + "number", + ); + assert.ok( + JSON.stringify(error).includes( + resource === "cpu" ? "SIGXCPU" : "EFBIG", + ), + ); + } else if (category === "execution_failure") { + assert.equal(error.code, "execution_failure"); + assert.ok(JSON.stringify(error).includes("launcher exit 76")); + assert.ok(JSON.stringify(error).includes("file_size_failure_marker")); + } else { + assert.equal(error.code, "capability_unavailable"); + assert.ok(JSON.stringify(error).includes(limiter)); + } + cases++; + } + } finally { + try { + await selectedClient.close(); + } finally { + await selectedTransport.close(); + } + } + } + for (const [name, encoding] of [ + ["plain.hex", "hex"], + ["via-ir.raw", "raw"], + ]) { + await execute( + strace, + [ + "-ff", + "-e", + "trace=network,execve,execveat", + "-s", + "4096", + "-o", + join(root.path, `offline.trace.${name}`), + process.execPath, + entrypoint, + "inspect-evm-interface", + join(root.path, name), + encoding, + "--json", + ], + { + env: { ...environment, PATH: "/usr/bin:/bin" }, + timeout: 45_000, + maxBuffer: 32 * 1024 * 1024, + }, + ); + } + const observedExecutables = new Set(); + for (const file of await readdir(root.path)) { + if (!file.startsWith("offline.trace.")) continue; + const localSocketDescriptors = new Set(); + for (const line of (await readFile(join(root.path, file), "utf8")).split( + "\n", + )) { + if (/execve(?:at)?\(/.test(line)) { + const match = /^execve\("([^"]+)"/.exec(line); + assert.notEqual(match, null, `Unresolved executable identity: ${line}`); + assert.ok( + line.endsWith(" = 0") || / = -1 [A-Z]+/.test(line), + `Incomplete exec observation: ${line}`, + ); + const ownershipInspection = + ["/usr/bin/ps", "/bin/ps"].includes(match[1]) && + line.includes( + '["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]', + ); + assert.ok( + [ + process.execPath, + environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit", + ].includes(match[1]) || ownershipInspection, + `Unexpected host execution: ${line}`, + ); + observedExecutables.add(match[1]); + } else if (/^[a-z]+\(/.test(line)) { + assert.ok( + permitsStdioSocketObservation(line, localSocketDescriptors), + `Unexpected attempted network operation: ${line}`, + ); + } + } + } + assert.ok(observedExecutables.has(process.execPath)); + assert.ok( + observedExecutables.has( + environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit", + ), + ); + cases += 2; +} catch (cause) { + failures.push(cause); +} finally { + for (const close of [ + () => client.close(), + () => transport.close(), + () => root.close(), + ]) { + try { + await close(); + } catch (cause) { + failures.push(cause); + } + } +} +const verifier = await completeVerifierRun(run); +if ( + verifier.process_lineage.status !== "verified" || + verifier.process_lineage.descendants.length !== 0 +) + failures.push( + new Error( + "Verifier process lineage was not fully released: " + + JSON.stringify(verifier), + ), + ); +console.log( + JSON.stringify( + { + status: failures.length === 0 ? "passed" : "failed", + public_cases: cases, + compiler: solc.version(), + engine: "evmole@0.9.3", + ...(failures.length === 0 + ? { + target_execution: "exec-syscalls-verified-absent", + network_requests: "socket-syscalls-verified-absent", + } + : {}), + verifier, + }, + null, + 2, + ), +); +if (failures.length !== 0) + throw new AggregateError( + failures, + "Offline EVM interface verification failed; cleanup failures retained.", + ); + +function permitsStdioSocketObservation(line, localDescriptors) { + if (line.startsWith("socketpair(AF_UNIX,")) { + const pair = /\[(\d+), (\d+)\]\)\s+= 0$/.exec(line); + if (pair !== null) { + localDescriptors.add(pair[1]); + localDescriptors.add(pair[2]); + } + return true; + } + const name = /^getsockname\((\d+), \{sa_family=AF_UNIX\}/.exec(line); + if (name !== null) { + localDescriptors.add(name[1]); + return true; + } + // A failed descriptor query performs no request and identifies no socket. + if (line.startsWith("getsockname(") && / = -1 ENOTSOCK/.test(line)) + return true; + const metadata = + /^(?:getsockopt\((\d+), SOL_SOCKET, SO_TYPE,|setsockopt\((\d+), SOL_SOCKET, SO_(?:RCVBUF|SNDBUF),|shutdown\((\d+), SHUT_WR\))/.exec( + line, + ); + const descriptor = metadata?.[1] ?? metadata?.[2] ?? metadata?.[3]; + // Allow descriptor queries/buffer configuration only after observing a local + // stdio socket. Other socket creation, connection and data operations fail. + return descriptor !== undefined && localDescriptors.has(descriptor); +} diff --git a/scripts/verify-native-calls.mjs b/scripts/verify-native-calls.mjs new file mode 100644 index 0000000000..f3b838d4d6 --- /dev/null +++ b/scripts/verify-native-calls.mjs @@ -0,0 +1,240 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +import { + artifactCli, + artifactMcpResult, + withArtifactMcp, +} from "./lib/artifact-e2e.mjs"; + +const exec = promisify(execFile); + +if (process.platform !== "darwin") + throw new Error( + "Native call observation verification requires macOS with Command Line Tools", + ); +for (const tool of ["clang", "lldb", "codesign", "nm"]) + try { + await exec("/usr/bin/xcrun", ["--find", tool]); + } catch (cause) { + throw new Error( + `Native call observation verification requires ${tool} from Command Line Tools (xcrun --find ${tool} failed)`, + { cause }, + ); + } + +const SOURCE = fileURLToPath( + new URL("../tests/conformance/native/calls.m", import.meta.url), +); +const GREET = "-[ReaCallGreeter greet:times:]"; +const INPUT = { + breakpoints: [ + { + kind: "objc-method", + class_name: "ReaCallGreeter", + selector: "greet:times:", + method_type: "instance", + }, + { kind: "function", name: "rea_call_add" }, + ], + environment: { REA_CALLS_MARKER: "observed" }, + argument_registers: 4, + backtrace_frames: 1, +}; + +/** Pointer-free facts that two runs of the same scenario must share. */ +const stable = (result) => + result.events.map((event) => ({ + symbol: event.symbol, + file_address: event.file_address, + selector: event.selector, + receiver_class: event.receiver_class, + counter: + event.symbol === GREET + ? event.registers[3].value + : event.registers[0].value, + })); + +const symbolAddress = async (binary, symbol) => { + const { stdout } = await exec("/usr/bin/xcrun", ["nm", "-n", binary]); + const line = stdout.split("\n").find((entry) => entry.endsWith(` ${symbol}`)); + assert.ok(line, `nm lists ${symbol}`); + return `0x${BigInt(`0x${line.split(" ")[0]}`).toString(16)}`; +}; + +const checkScenario = async (binary) => { + const result = await artifactCli("observe-native-calls", binary, [ + JSON.stringify(INPUT), + ]); + assert.equal(result.process.outcome, "exited"); + assert.equal(result.process.exit_status, 0); + assert.equal(result.process.terminated, true); + assert.match(result.process.stdout.text, /^world x0 40\n/u); + assert.match(result.process.stdout.text, /marker observed\n$/u); + assert.deepEqual(result.coverage, { + status: "complete", + event_limit_reached: false, + unresolved_breakpoints: [], + }); + const greet = result.events.filter(({ symbol }) => symbol === GREET); + const add = result.events.filter(({ symbol }) => symbol === "rea_call_add"); + assert.equal(greet.length, 3); + assert.equal(add.length, 3); + for (const [index, event] of greet.entries()) { + assert.equal(event.receiver_class, "ReaCallGreeter"); + assert.equal(event.selector, "greet:times:"); + assert.equal(event.registers[3].value, `0x${index}`); + assert.equal(event.backtrace[0].symbol, "main"); + assert.equal(event.module, basename(binary)); + } + for (const [index, event] of add.entries()) { + assert.equal(event.registers[0].value, `0x${index}`); + assert.equal(event.registers[1].value, "0x28"); + } + // Breakpoints stop at the symbol itself, which Apple's nm reports. + assert.equal( + add[0].file_address, + await symbolAddress(binary, "_rea_call_add"), + ); + assert.equal(greet[0].file_address, await symbolAddress(binary, GREET)); + return result; +}; + +const alive = (pid) => { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}; + +const checkBounds = async (binary) => { + const limited = await artifactCli("observe-native-calls", binary, [ + JSON.stringify({ + breakpoints: [{ kind: "function", name: "nanosleep" }], + arguments: ["--wait"], + max_events: 2, + duration_ms: 30_000, + }), + ]); + assert.equal(limited.process.outcome, "event-limit"); + assert.equal(limited.events.length, 2); + assert.equal(limited.process.terminated, true); + assert.equal(alive(limited.process.pid), false); + const elapsed = await artifactCli("observe-native-calls", binary, [ + JSON.stringify({ + breakpoints: [{ kind: "function", name: "rea_call_add" }], + arguments: ["--wait"], + duration_ms: 1_500, + }), + ]); + assert.equal(elapsed.process.outcome, "duration-elapsed"); + assert.equal(elapsed.events.length, 0); + assert.equal(alive(elapsed.process.pid), false); +}; + +const cliFailure = async (binary) => { + try { + await exec( + process.execPath, + [ + fileURLToPath(new URL("rea.mjs", import.meta.url)), + "observe-native-calls", + binary, + JSON.stringify(INPUT), + "--json", + ], + { env: { ...process.env, REA_LOG_LEVEL: "silent" }, timeout: 120_000 }, + ); + } catch (cause) { + return JSON.parse(cause.stdout); + } + throw new Error("observe-native-calls unexpectedly succeeded"); +}; + +/** Hardened runtime blocks the debugger unless get-task-allow is granted. */ +const checkHardenedRuntime = async (directory, binary) => { + const blocked = join(directory, "hardened"); + await exec("/bin/cp", [binary, blocked]); + await exec("/usr/bin/xcrun", [ + "codesign", + "-f", + "-s", + "-", + "-o", + "runtime", + blocked, + ]); + const denied = await cliFailure(blocked); + assert.equal(denied.code, "capability_unavailable"); + assert.match(denied.details.reason, /^debugger-attach-denied: /u); + const allowed = join(directory, "debuggable"); + const entitlements = join(directory, "get-task-allow.plist"); + await writeFile( + entitlements, + 'com.apple.security.get-task-allow', + ); + await exec("/bin/cp", [binary, allowed]); + await exec("/usr/bin/xcrun", [ + "codesign", + "-f", + "-s", + "-", + "-o", + "runtime", + "--entitlements", + entitlements, + allowed, + ]); + const traced = await checkScenario(allowed); + assert.equal(traced.process.exit_status, 0); +}; + +const directory = await mkdtemp(join(tmpdir(), "rea-native-calls-")); +try { + const binary = join(directory, "ReaCalls"); + await exec("/usr/bin/xcrun", [ + "clang", + "-fobjc-arc", + "-framework", + "Foundation", + SOURCE, + "-o", + binary, + ]); + const viaCli = await checkScenario(binary); + await withArtifactMcp(binary, async (client) => { + const viaMcp = await artifactMcpResult( + client, + "observe_native_calls", + INPUT, + ); + assert.deepEqual(stable(viaMcp), stable(viaCli)); + assert.equal(viaMcp.process.stdout.text, viaCli.process.stdout.text); + }); + await checkBounds(binary); + await checkHardenedRuntime(directory, binary); + console.log( + JSON.stringify({ + ok: true, + mocked: false, + cli: true, + stdio_mcp: true, + debugger: viaCli.debugger.version, + events: viaCli.events.length, + outcomes: ["exited", "event-limit", "duration-elapsed"], + hardened_runtime: { + without_get_task_allow: "denied", + with_get_task_allow: "traced", + }, + }), + ); +} finally { + await rm(directory, { recursive: true, force: true }); +} diff --git a/scripts/verify-native-ui.mjs b/scripts/verify-native-ui.mjs index f76af57d16..34616a72a9 100644 --- a/scripts/verify-native-ui.mjs +++ b/scripts/verify-native-ui.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { observeNativeUi } from "../dist/native/NativeUiObservation.js"; +import { NATIVE_UI_HELPER_MAX_BUFFER } from "../dist/native/NativeUiOutputBudget.js"; if (process.platform !== "darwin") throw new Error( @@ -30,7 +31,38 @@ try { "-o", childRetrievalTest, ]); - await promisify(execFile)(childRetrievalTest, []); + const childRetrieval = await promisify(execFile)(childRetrievalTest, []); + const [slashRichProducerJson, numericProducerJson] = childRetrieval.stdout + .trimEnd() + .split("\n"); + if ( + slashRichProducerJson === undefined || + numericProducerJson === undefined + ) { + throw new Error( + "Native UI serializer seam did not emit its producer bytes", + ); + } + const slashRichConsumerJson = JSON.stringify( + JSON.parse(slashRichProducerJson), + ); + if (slashRichProducerJson !== slashRichConsumerJson) { + throw new Error( + "Swift helper JSON bytes differ from the normalized JavaScript consumer representation", + ); + } + const numericConsumerJson = JSON.stringify(JSON.parse(numericProducerJson)); + if ( + Buffer.byteLength(numericProducerJson) <= + Buffer.byteLength(numericConsumerJson) + ) { + throw new Error( + "Foundation numeric fixture no longer exercises larger producer JSON bytes", + ); + } + if (Buffer.byteLength(numericProducerJson) > NATIVE_UI_HELPER_MAX_BUFFER) { + throw new Error("Foundation numeric fixture exceeds the raw helper budget"); + } const contents = join(root, "Fixture.app", "Contents"); await mkdir(join(contents, "MacOS"), { recursive: true }); diff --git a/scripts/verify-package-install.mjs b/scripts/verify-package-install.mjs index da2e79f24f..81ee5afc66 100644 --- a/scripts/verify-package-install.mjs +++ b/scripts/verify-package-install.mjs @@ -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( diff --git a/scripts/verify-package-mcp.mjs b/scripts/verify-package-mcp.mjs index a4831d8253..3b5be1b5d6 100644 --- a/scripts/verify-package-mcp.mjs +++ b/scripts/verify-package-mcp.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { readFile, rm } from "node:fs/promises"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; @@ -167,6 +168,7 @@ const verifyMcpInlineArtifactEvidence = async ( mcpOptions, ); assert.notEqual(opened.isError, true, JSON.stringify(opened)); + let outputRoot; try { const inspected = await client.callTool( { name: "inspect_artifact", arguments: {} }, @@ -212,12 +214,35 @@ const verifyMcpInlineArtifactEvidence = async ( evidence, ); } - } finally { - const closed = await client.callTool( - { name: "close_binary", arguments: {} }, + const extracted = await client.callTool( + { name: "extract_artifact", arguments: {} }, mcpOptions, ); - assert.notEqual(closed.isError, true, JSON.stringify(closed)); + assert.notEqual(extracted.isError, true, JSON.stringify(extracted)); + const extraction = parseEvidence(extracted.structuredContent?.evidence); + assert.deepEqual( + extraction.normalized_result, + extracted.structuredContent?.result, + ); + const materialized = extracted.structuredContent?.result; + assert.equal(typeof materialized?.output_root, "string"); + outputRoot = materialized.output_root; + assert.equal(materialized.containment_verified, true); + assert.equal( + await readFile(join(outputRoot, "app/main.js"), "utf8"), + "main();", + ); + } finally { + try { + const closed = await client.callTool( + { name: "close_binary", arguments: {} }, + mcpOptions, + ); + assert.notEqual(closed.isError, true, JSON.stringify(closed)); + } finally { + if (outputRoot !== undefined) + await rm(outputRoot, { recursive: true, force: true }); + } } }; diff --git a/scripts/verify-package-pack.mjs b/scripts/verify-package-pack.mjs index 8cae7bf4a2..600ecf7a03 100644 --- a/scripts/verify-package-pack.mjs +++ b/scripts/verify-package-pack.mjs @@ -1,4 +1,5 @@ import { join } from "node:path"; +import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js"; import { exec } from "./lib/verify-package-core.mjs"; import { verifyPackedBridge } from "./verify-packed-bridge.mjs"; @@ -28,6 +29,26 @@ export async function verifyPackagePack({ root, workspace }) { throw new Error("package did not expose both rea command entry points"); if (packedManifest.mcpName !== "io.github.morluto/rea") throw new Error("package did not retain its MCP Registry ownership marker"); + if (packedFiles.some((path) => path.startsWith("package/skill-src/"))) + throw new Error( + "package included authored skill sources instead of only the generated bundle", + ); + const skill = ( + await exec("tar", [ + "-xOf", + join(root, tarball), + "package/skills/reverse-engineer-anything/SKILL.md", + ]) + ).stdout; + if ( + !skill.includes( + ` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}\n`, + ) || + skill.includes(" catalog_digest:") + ) + throw new Error( + "packaged skill metadata did not match the catalog shipped in the package", + ); if ( packedFiles.some( (path) => path.includes("__pycache__") || path.endsWith(".pyc"), diff --git a/scripts/verify-packaged-ghidra-windows.mjs b/scripts/verify-packaged-ghidra-windows.mjs index 1dfe3cd34d..fa17c856cd 100644 --- a/scripts/verify-packaged-ghidra-windows.mjs +++ b/scripts/verify-packaged-ghidra-windows.mjs @@ -25,10 +25,12 @@ if (process.platform !== "win32" || process.arch !== "x64") ); const workspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-")); const exec = promisify(execFile); -let packageRoot = resolve(process.argv[2] ?? "."); +const x86Fixture = process.argv[2] === "--x86"; +const packageRootArgument = x86Fixture ? undefined : process.argv[2]; +let packageRoot = resolve(packageRootArgument ?? "."); // The default lane verifies the npm artifact in an isolated prefix. Explicit // package roots support an already-installed artifact without a second install. -if (process.argv[2] === undefined) { +if (packageRootArgument === undefined) { try { const npmEntry = process.env.npm_execpath ?? @@ -77,7 +79,10 @@ if (process.argv[2] === undefined) { } } const target = resolve( - process.argv[3] ?? "build/fixtures/rea-ghidra-windows.exe", + process.argv[3] ?? + (x86Fixture + ? "build/fixtures/rea-ghidra-windows-x86.exe" + : "build/fixtures/rea-ghidra-windows.exe"), ); let TOOL_CONTRACTS, native, token, sha256; try { @@ -215,7 +220,8 @@ try { }; try { await client.connect(transport); - await call("open_binary", { path: target, provider_id: "ghidra" }); + await call("open_binary", { path: target }); + await call("binary_overview", {}); assert.equal((await call("list_documents", {})).length, 1); await call("list_names", {}); const procedures = await call("list_procedures", {}); diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs index 8dd9f6b53a..f245a4a9f9 100644 --- a/scripts/verify-real-ghidra-windows.mjs +++ b/scripts/verify-real-ghidra-windows.mjs @@ -63,12 +63,20 @@ if ( ); const root = resolve(fileURLToPath(new URL("..", import.meta.url))); -const targetPath = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe"); +const architecture = process.argv[2] === "--x86" ? "x86" : "x86_64"; +const targetPath = resolve( + root, + "build", + "fixtures", + architecture === "x86" + ? "rea-ghidra-windows-x86.exe" + : "rea-ghidra-windows.exe", +); const target = await parseBinaryTarget(targetPath); if ( !target.ok || target.value.format !== "pe" || - target.value.architecture !== "x86_64" || + target.value.architecture !== architecture || target.value.executableRole !== "application" || target.value.managed !== false ) @@ -113,6 +121,9 @@ try { target.value.sha256, profile.value.profile.digest, ); + const languagePrefix = architecture === "x86" ? "x86:LE:32:" : "x86:LE:64:"; + if (!started.value.target.language_id.startsWith(languagePrefix)) + throw new Error("Windows fixture imported with the wrong Ghidra language"); coordinates = client.diagnostics(); assertRuntimeCoordinates(coordinates, target.value.sha256); @@ -137,6 +148,19 @@ try { ); if (procedure === undefined) throw new Error("Windows Ghidra fixture exposed no local procedure"); + const calleeAddress = architecture === "x86" ? 0x401010n : 0x140001010n; + const callee = procedures.find( + (item) => BigInt(item.address) === calleeAddress, + ); + if (callee === undefined) + throw new Error("Windows fixture exposed no controlled callee"); + const pseudoCode = await functionOperation("procedure_pseudo_code", { + procedure: callee.value, + }); + if (typeof pseudoCode !== "string" || !/\b(?:42|0x2a)\b/iu.test(pseudoCode)) + throw new Error( + "Windows fixture decompilation lost its known return value", + ); await inventory("inspect_native_load_image", {}); const memory = await inventory("read_bytes", { @@ -210,6 +234,7 @@ try { report = { ok: true, provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION }, + language_id: started.value.target.language_id, target: { format: target.value.format, architecture: target.value.architecture, @@ -228,7 +253,7 @@ try { limitations: [ "approved-non-sensitive-fixtures-only", "windows-x64-local-ntfs-only", - "native-x86-64-pe-applications-only", + "native-x86-and-x86-64-pe-applications-only", "no-gui-or-mutation-authority", ], }; diff --git a/scripts/verify-recorded-crash.mjs b/scripts/verify-recorded-crash.mjs new file mode 100644 index 0000000000..eb59749c73 --- /dev/null +++ b/scripts/verify-recorded-crash.mjs @@ -0,0 +1,318 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { writeFile, readFile, copyFile } from "node:fs/promises"; +import { isAbsolute, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js"; +import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; +import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs"; +import { connectRecordedCrash } from "./verify/recorded-crash/public.mjs"; +import { + createRecordedCore, + fixtureCommand, + startCoreSentinel, +} from "./verify/recorded-crash/fixtures.mjs"; +import { verifyCoreInspectionTrace } from "./verify/recorded-crash/trace.mjs"; + +const environment = Object.fromEntries( + Object.entries(process.env).filter(([, value]) => typeof value === "string"), +); +environment.REA_PWNDBG_GDB ??= "/usr/bin/gdb"; +for (const key of [ + "REA_PWNTOOLS_PYTHON", + "REA_PWNDBG_GDB", + "REA_PWNDBG_GDBINIT", + "REA_PWNDBG_VENV_PATH", + "REA_VERIFY_STRACE_COMMAND", +]) + if (!isAbsolute(environment[key] ?? "")) + throw new Error( + `verify:recorded:crash requires absolute ${key}; see docs/recorded-crashes.md. No dependencies are installed by this verifier.`, + ); +if (process.platform !== "linux" || process.arch !== "x64") + throw new Error("verify:recorded:crash requires Linux x64."); +const run = createVerifierRun(); +environment.REA_PROCESS_RUN_ID = run.run_id; +const root = await PrivateRuntimeRoot.create({ prefix: "rea-core-verifier-" }); +const options = { root: root.path, environment, runId: run.run_id }; +const entrypoint = + process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url)); +const failures = []; +const sessions = []; +let sentinel; +let cases = 0; +let trace; +let catalogTools; +try { + for (const [command, args] of [ + ["gcc", ["--version"]], + [environment.REA_PWNDBG_GDB, ["--version"]], + [environment.REA_VERIFY_STRACE_COMMAND, ["-V"]], + ]) { + try { + await fixtureCommand(command, args, options); + } catch (cause) { + throw new Error( + `verify:recorded:crash prerequisite unavailable: ${command}`, + { cause }, + ); + } + } + const core = await createRecordedCore(options); + sentinel = await startCoreSentinel(options); + sentinel.assertAlive(); + const fixtures = JSON.parse( + await fixtureCommand( + environment.REA_PWNTOOLS_PYTHON, + [ + "-I", + fileURLToPath( + new URL("./lib/recorded-core-fixtures.py", import.meta.url), + ), + core, + root.path, + String(sentinel.pid), + ], + options, + ), + ); + const spaced = join(root.path, "recording with spaces.core"); + await copyFile(core, spaced); + const basic = await connectRecordedCrash({ entrypoint, environment }); + sessions.push(basic); + const advertised = (await basic.client.listTools()).tools; + assert.equal( + advertised.filter((tool) => tool.name === "inspect_recorded_crash").length, + 1, + ); + const catalog = await verifyCompleteToolCatalog(basic.client, { + timeout: 90_000, + }); + for (const name of [ + "inspect_binary_layout", + "inspect_recorded_crash", + "inspect_evm_interface", + ]) + assert.ok(catalog.includes(name)); + catalogTools = catalog.length; + for (const mode of ["cli", "mcp"]) { + const report = await basic.inspect(mode, spaced); + assert.equal(report.threads.length, 2); + assert.ok( + report.threads.some((thread) => + thread.registers.some( + (register) => + register.name === "rdi" && register.value === "0x1122334455667788", + ), + ), + ); + assert.ok( + report.threads.some((thread) => + thread.registers.some( + (register) => + register.name === "r12" && register.value === "0xfedcba9876543210", + ), + ), + ); + assert.ok( + report.signals.some( + (signal) => + signal.number === 11 && + signal.code === 1 && + signal.fault_address === "0x123", + ), + ); + assert.ok( + report.signals.some( + (signal) => signal.code === -6 && signal.fault_address === null, + ), + ); + cases++; + const enriched = await basic.inspect(mode, spaced, { + debuggerContext: true, + }); + assert.deepEqual(enriched.threads, report.threads); + assert.deepEqual(enriched.notes, report.notes); + assert.ok(enriched.debugger.pwndbg_version.startsWith("2026.09.15")); + assert.equal(enriched.debugger.connection, "core"); + assert.equal(enriched.debugger.executable, null); + assert.ok(enriched.debugger.maps.length > 0); + assert.ok( + enriched.debugger.maps.every( + (map) => map.reported_flags !== 0 || map.permissions === null, + ), + ); + assert.ok( + enriched.debugger.maps.every( + (map) => map.current_file_identity === "unknown", + ), + ); + assert.ok( + enriched.debugger.reported_limits.address_space_bytes <= 3 * 1024 ** 3, + ); + assert.ok(enriched.debugger.reported_limits.cpu_seconds <= 30); + assert.ok( + enriched.debugger.reported_limits.file_size_bytes <= 64 * 1024 ** 2, + ); + assert.ok(enriched.diagnostics.stdout.includes("[core decoder]")); + assert.equal(enriched.decoder_diagnostics.truncated, false); + cases++; + for (const name of [ + "truncated-header", + "truncated-status", + "truncated-owner", + "nonzero-note-tail", + ]) { + await basic.inspect(mode, fixtures[name], { category: "invalid_input" }); + cases++; + } + const opaque = await basic.inspect(mode, fixtures["opaque-owner"]); + assert.ok( + opaque.notes.some( + (note) => Buffer.from(note.owner_bytes_base64, "base64")[0] === 255, + ), + ); + assert.equal(opaque.threads.length, 1); + cases++; + const noSignals = await basic.inspect(mode, fixtures["unknown-signals"]); + assert.equal(noSignals.signals.length, 0); + assert.equal(noSignals.threads.length, 2); + cases++; + const noNotes = await basic.inspect(mode, fixtures["missing-notes"]); + assert.equal(noNotes.notes.length, 0); + assert.equal(noNotes.threads.length, 0); + assert.equal(noNotes.note_interpretation_completeness, "unknown"); + cases++; + const negative = await basic.inspect(mode, fixtures["negative-pid"]); + assert.ok(negative.threads.some((thread) => thread.historical_pid === -2)); + cases++; + const linux = await basic.inspect(mode, fixtures["linux-osabi"]); + assert.deepEqual(linux.threads, report.threads); + assert.deepEqual(linux.signals, report.signals); + cases++; + const extended = await basic.inspect( + mode, + fixtures["extended-program-count"], + ); + assert.deepEqual(extended.threads, report.threads); + assert.deepEqual(extended.signals, report.signals); + cases++; + const padded = await basic.inspect(mode, fixtures["zero-note-tail"]); + assert.equal(padded.threads.length, 2); + assert.ok( + padded.note_padding.some((padding) => padding.bytes_base64 === "AAAA"), + ); + cases++; + for (const name of [ + "solaris-osabi", + "freebsd-osabi", + "unsupported-abi-version", + ]) { + await basic.inspect(mode, fixtures[name], { + category: "unsupported_target", + }); + cases++; + } + const collision = await basic.inspect( + mode, + fixtures["historical-pid-collision"], + { debuggerContext: true }, + ); + assert.ok( + collision.threads.some( + (thread) => thread.historical_pid === sentinel.pid, + ), + ); + sentinel.assertAlive(); + cases++; + } + for (const [name, changes] of [ + ["missing", { REA_PWNDBG_GDBINIT: join(root.path, "missing-plugin.py") }], + [ + "wrong-profile", + { REA_PWNDBG_GDBINIT: join(root.path, "wrong-profile.py") }, + ], + ]) { + if (name === "wrong-profile") { + // Source-owned failure injection changes only the in-memory reported version. + // The selected upstream checkout and its installed files remain unchanged. + await writeFile( + changes.REA_PWNDBG_GDBINIT, + `from pathlib import Path\nentry = ${JSON.stringify(environment.REA_PWNDBG_GDBINIT)}\nexec(compile(Path(entry).read_bytes(), entry, 'exec'), {'__name__': '__main__', '__file__': entry})\nimport pwndbg.lib.version\npwndbg.lib.version.__version__ = '0.0.0-source-owned'\n`, + ); + } + const session = await connectRecordedCrash({ + entrypoint, + environment: { ...environment, ...changes }, + }); + sessions.push(session); + for (const mode of ["cli", "mcp"]) { + const error = await session.inspect(mode, core, { + debuggerContext: true, + category: "unsupported_provider", + }); + assert.ok( + JSON.stringify(error).includes( + name === "missing" ? "REA_PWNDBG_GDBINIT" : "source-owned", + ), + ); + cases++; + } + } + const executableCarrier = join(root.path, "executable-as-core"); + const executableBytes = Buffer.from(await readFile(core)); + executableBytes.writeUInt16LE(2, 16); + await writeFile(executableCarrier, executableBytes); + for (const mode of ["cli", "mcp"]) { + const error = await basic.inspect(mode, executableCarrier, { + category: "unsupported_target", + }); + assert.ok(JSON.stringify(error).includes("ET_CORE")); + cases++; + } + trace = await verifyCoreInspectionTrace({ + ...options, + entrypoint, + core: fixtures["historical-pid-collision"], + sentinel, + }); + cases += trace.length; + assert.ok((await readFile(core)).length > 0); +} catch (cause) { + failures.push(cause); +} finally { + for (const close of [ + ...sessions.map((session) => () => session.close()), + ...(sentinel ? [() => sentinel.close()] : []), + () => root.close(), + ]) { + try { + await close(); + } catch (cause) { + failures.push(cause); + } + } +} +const verifier = await completeVerifierRun(run); +try { + assert.equal(verifier.process_lineage.status, "verified"); + assert.deepEqual(verifier.process_lineage.descendants, []); +} catch (cause) { + failures.push(cause); +} +const result = { + status: failures.length === 0 ? "passed" : "failed", + public_cases: cases, + catalog_tools: catalogTools, + profile: + "Linux x64; pwntools4.15.0/pyelftools0.33/Unicorn2.1.2; core-only BYO GDB/pwndbg2026.09.15", + fixture_generation: "owned test program executed", + inspection_trace: trace, + verifier, +}; +console.log(JSON.stringify(result, null, 2)); +if (failures.length > 0) + throw new AggregateError( + failures, + "Recorded crash verification failed; cleanup failures are retained.", + ); diff --git a/scripts/verify-release-checkpoint.mjs b/scripts/verify-release-checkpoint.mjs new file mode 100644 index 0000000000..e8ce6329ef --- /dev/null +++ b/scripts/verify-release-checkpoint.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import { execFile } from "node:child_process"; +import { parseArgs, promisify } from "node:util"; +import { inspectReleaseCheckpoint } from "./lib/release-checkpoint.mjs"; + +const execFileAsync = promisify(execFile); + +async function git(args) { + const result = await execFileAsync("git", args, { + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + }); + return result.stdout; +} + +async function main() { + const { values } = parseArgs({ + options: { + phase: { type: "string" }, + stage: { type: "string", default: "source" }, + "release-branch": { type: "string" }, + "source-sha": { type: "string" }, + }, + }); + const report = await inspectReleaseCheckpoint(git, { + phase: values.phase, + stage: values.stage, + releaseBranch: values["release-branch"], + sourceSha: values["source-sha"], + }); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + if (report.missingNotes.length > 0) { + process.stderr.write( + `Review ${report.missingNotes.length} additional unreleased mainline entries missing from these release notes; see the ancestry report above.\n`, + ); + } +} + +await main().catch((error) => { + process.stderr.write( + `Release checkpoint validation failed: ${error instanceof Error ? error.message : String(error)}\n`, + ); + process.exitCode = 1; +}); diff --git a/scripts/verify-web-network-captures.mjs b/scripts/verify-web-network-captures.mjs new file mode 100644 index 0000000000..316d811ce9 --- /dev/null +++ b/scripts/verify-web-network-captures.mjs @@ -0,0 +1,996 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash, randomUUID } from "node:crypto"; +import { + access, + copyFile, + chmod, + readFile, + truncate, + writeFile, +} from "node:fs/promises"; +import { dirname, isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js"; +import { runOwnedCommand } from "../dist/process/OwnedCommand.js"; +import { WEB_NETWORK_CAPTURE_LIMITS } from "../dist/domain/webNetworkCapture.js"; +import { mcpTextValue } from "./lib/mcp-verifier-results.mjs"; +import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; + +const executable = process.env.REA_MITMDUMP_COMMAND; +if ( + process.platform !== "linux" || + executable === undefined || + !isAbsolute(executable) +) + throw new Error( + "verify:web:network-captures requires Linux and absolute REA_MITMDUMP_COMMAND for caller-supplied mitmdump 12.2.3", + ); +await access(executable); +const entrypoint = + process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url)); +const run = createVerifierRun(); +const runtime = await PrivateRuntimeRoot.create({ + prefix: "rea-history-verifier-", +}); +const environment = Object.fromEntries( + Object.entries(process.env).filter(([, value]) => typeof value === "string"), +); +const client = new Client({ + name: "historical-web-capture-verifier", + version: "1", +}); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: environment, + stderr: "pipe", +}); +const failures = []; +let cases = 0; +try { + await runOwnedCommand( + { + command: executable, + arguments: [ + "--no-server", + "-q", + "--set", + `confdir=${join(runtime.path, "config")}`, + "--set", + `rea_fixture_root=${runtime.path}`, + "--set", + `rea_adapter_path=${resolve(dirname(entrypoint), "../bridge/mitmproxy/capture.py")}`, + "-s", + fileURLToPath( + new URL("./fixtures/generate-mitmproxy-capture.py", import.meta.url), + ), + ], + cwd: runtime.path, + runId: `rea-history-fixture-${randomUUID()}`, + hostEnvironment: environment, + }, + { timeoutMs: 30_000, diagnosticBytes: 1024 * 1024 }, + ); + assert.deepEqual( + JSON.parse( + await readFile( + join(runtime.path, "native-error-classifications.json"), + "utf8", + ), + ), + { passed: 7 }, + ); + await client.connect(transport); + for (const format of ["har", "mitmproxy"]) { + const path = join( + runtime.path, + format === "har" ? "producer.har" : "flows.mitm", + ); + const raw = await readFile(path); + const input = { capture_path: path, format, record_ordinals: [1, 0] }; + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, input); + assert.equal(value.total_records, 2); + assert.deepEqual( + value.records.map((record) => record.ordinal), + [1, 0], + ); + assert.equal( + value.artifact.sha256, + createHash("sha256").update(raw).digest("hex"), + ); + assert.equal(value.runtime_attribution, "unknown"); + const first = value.records[1]; + if (format === "mitmproxy") { + assert.equal(first.reported.id, "original-producer-id"); + assert.equal(value.records[0].reported.id, first.reported.id); + assert.equal(first.location.offset, 0); + assert.equal( + value.records[0].location.offset + value.records[0].location.bytes, + raw.length, + ); + assert.equal( + first.reported.request.path, + "http://example.test/a?token=ordinary#fragment", + ); + assertBinary( + first, + "/request/content", + Buffer.from([0, 255, ...Buffer.from("body")]), + ); + assertBinary( + first, + "/response/content", + Buffer.from([0, 254, ...Buffer.from("answer")]), + ); + assertBinary( + first, + "/websocket/messages/0/2", + Buffer.from([0, 253, ...Buffer.from("message")]), + ); + assert.equal(value.records[0].reported.request.content, null); + assertBinary(value.records[0], "/response/content", Buffer.alloc(0)); + assert.ok( + first.numeric_literals.some( + (number) => + number.pointer === "/metadata/big_integer" && + number.literal === "9007199254740993", + ), + ); + assert.ok( + first.numeric_literals.some( + (number) => + number.pointer === "/metadata/nonfinite" && + number.literal === "inf", + ), + ); + assert.equal( + first.reported.metadata.response.headers[0][1], + "ordinary-extension-credential-name", + ); + } else { + assert.equal(value.container.reported.log.creator.name, "mitmproxy"); + assert.equal(value.container.reported.log.creator.version, "12.2.3"); + assert.equal(value.records[0].reported.request.postData.text, null); + assertBinary( + first, + "/response/content/text", + Buffer.from([0, 255, 1, 254]), + ); + } + const text = JSON.stringify(value); + for (const secret of [ + "native-transport-secret", + "cookie-secret", + "redirect-password", + "user:password", + ]) + assert.ok( + !text.includes(secret), + `Credential leaked through ${mode}/${format}`, + ); + cases++; + } + } + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { + capture_path: join(runtime.path, "string-urls.mitm"), + format: "mitmproxy", + }); + const first = value.records[0]; + for (const state of [first.reported, first.reported.backup]) { + assert.equal(state.request.path, "https://example.test/string-path"); + assert.equal(state.request.authority, "example.test"); + assert.deepEqual( + state.request.headers.map((field) => field[1]), + ["https://example.test/from", "//example.test"], + ); + assert.equal(state.response.headers[0][1], "https://example.test/to"); + } + assert.ok(!JSON.stringify(value).includes("password")); + assert.ok( + first.redactions.some( + (redaction) => + redaction.pointer === "/backup/request/authority" && + redaction.reason === "transport-credential", + ), + ); + cases++; + } + for (const sensitive_values of [ + ["REDACTED", "["], + ["secret", "REDACTED"], + ]) { + for (const format of ["har", "mitmproxy"]) { + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { + capture_path: join( + runtime.path, + format === "har" ? "producer.har" : "string-urls.mitm", + ), + format, + record_ordinals: [0], + sensitive_values, + }); + const first = value.records[0]; + const markers = + format === "har" + ? first.reported._markers + : first.reported.metadata._markers; + assert.equal(markers.sensitive, null); + assert.equal(markers.ordinary, "unmarked"); + assert.equal( + sensitive_values.includes("[") ? markers.bracket : markers.overlap, + null, + ); + if (format === "mitmproxy") + assert.equal(first.reported.backup.metadata._markers.sensitive, null); + cases++; + } + } + } + for (const name of [ + "equal-number", + "equal-null", + "equal-object", + "escaped-key", + "core", + ]) { + const path = join(runtime.path, `duplicate-${name}.har`); + const original = await readFile(path); + for (const mode of ["cli", "mcp"]) { + await inspect( + mode, + { capture_path: path, format: "har" }, + "invalid_input", + ); + assert.deepEqual(await readFile(path), original); + cases++; + } + } + for (const format of ["har", "mitmproxy"]) { + const path = join( + runtime.path, + format === "har" ? "prototype.har" : "prototype.mitm", + ); + const original = await readFile(path); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { capture_path: path, format }); + const reported = + format === "har" + ? value.container.reported._extension + : value.records[0].reported.metadata; + assert.equal(Object.hasOwn(reported, "__proto__"), true); + assert.deepEqual(reported["__proto__"], { preserved: 7 }); + assert.equal(Object.getPrototypeOf(reported), Object.prototype); + assert.equal(Reflect.get(Object.prototype, "preserved"), undefined); + if (format === "har") assert.equal(reported.constructor, "ordinary"); + assert.deepEqual(await readFile(path), original); + cases++; + } + } + for (const format of ["har", "mitmproxy"]) { + const path = join(runtime.path, `malformed-${format}`); + await writeFile(path, format === "har" ? "{invalid-json" : "999999999999:"); + for (const mode of ["cli", "mcp"]) { + await inspect(mode, { capture_path: path, format }, "invalid_input"); + cases++; + } + } + for (const format of ["har", "mitmproxy"]) { + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { + capture_path: join( + runtime.path, + format === "har" ? "producer.har" : "string-urls.mitm", + ), + format, + sensitive_values: ["private-property"], + }); + const record = value.records[0]; + const properties = + format === "har" + ? record.reported._private_properties + : record.reported.metadata._private_properties; + assert.deepEqual(properties, { kept: 7 }); + assert.ok(!JSON.stringify(value).includes("private-property")); + assert.ok( + record.redactions.some( + (item) => + item.scope === "property-name" && + item.pointer === + (format === "har" + ? "/_private_properties" + : "/metadata/_private_properties"), + ), + ); + if (format === "mitmproxy") + assert.deepEqual(record.reported.backup.metadata._private_properties, { + kept: 7, + }); + cases++; + } + } + for (const [fixture, prefix] of [ + ["ows", " \t"], + ["controls", "\r\n \v\f\x00\x1f"], + ]) { + for (const format of ["har", "mitmproxy"]) { + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { + capture_path: join( + runtime.path, + `${fixture}.${format === "har" ? "har" : "mitm"}`, + ), + format, + }); + assert.ok(!JSON.stringify(value).includes("ows-secret")); + const record = value.records[0]; + const states = + format === "har" + ? [record.reported] + : [record.reported, record.reported.backup]; + for (const state of states) { + if (format === "har") { + for (const side of ["request", "response"]) + assert.ok( + state[side].headers.some( + (header) => + header.value === + `${prefix}HTTPS://example.test/path?token=ordinary#fragment\t `, + ), + ); + } else { + assert.deepEqual(state.request.headers, [ + [ + "Referer", + `${prefix}HTTPS://example.test/path?token=ordinary#fragment\t `, + ], + [ + "Origin", + `${prefix}//example.test/path?token=ordinary#fragment\t `, + ], + ]); + assert.deepEqual(state.response.headers, [ + [ + "Location", + `${prefix}HTTPS://example.test/path?token=ordinary#fragment\t `, + ], + ]); + } + } + if (format === "mitmproxy") { + for (const pointer of [ + "/request/headers/0/1", + "/backup/request/headers/0/1", + ]) + assert.deepEqual( + record.binary_fields.find((field) => field.pointer === pointer), + { + pointer, + representation: "producer-bytes", + state: "redacted", + content_base64: null, + bytes: null, + sha256: null, + }, + ); + } + cases++; + } + } + } + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { + capture_path: join(runtime.path, "non-utf8-urls.mitm"), + format: "mitmproxy", + }); + const record = value.records[0]; + assert.ok(!JSON.stringify(value).includes("byte-secret")); + for (const prefix of ["", "/backup"]) { + for (const suffix of [ + "/request/path", + "/request/authority", + "/request/headers/0/1", + "/request/headers/1/1", + "/response/headers/0/1", + ]) { + const pointer = prefix + suffix; + assert.deepEqual( + record.binary_fields.find((field) => field.pointer === pointer), + { + pointer, + representation: "producer-bytes", + state: "redacted", + content_base64: null, + bytes: null, + sha256: null, + }, + ); + } + } + cases++; + } + const nativeCoordinateInput = { + capture_path: join(runtime.path, "flows.mitm"), + format: "mitmproxy", + }; + const nativeCoordinates = await inspect("mcp", nativeCoordinateInput); + const originalLocation = nativeCoordinates.records[1].location; + assert.equal(originalLocation.kind, "byte-range"); + for (const literal of [ + String(originalLocation.offset), + String(originalLocation.bytes), + ]) { + for (const mode of ["cli", "mcp"]) { + const excluded = await inspect(mode, { + ...nativeCoordinateInput, + sensitive_values: [literal], + }); + assert.deepEqual(excluded.records[1].location, { + kind: "unknown", + reason: "explicit-sensitive-value", + }); + cases++; + } + } + cases++; + const deniedPath = join(runtime.path, "unreadable.har"); + await copyFile(join(runtime.path, "producer.har"), deniedPath); + await chmod(deniedPath, 0o000); + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + const failure = await inspect( + mode, + { capture_path: deniedPath, format }, + "unavailable", + ); + assert.equal(failure.code, "access_denied"); + assert.equal(failure.details.system_code, "EACCES"); + assert.equal(failure.details.path, deniedPath); + cases++; + } + for (const literal of ["log", "entries"]) { + const value = await inspect(mode, { + capture_path: join(runtime.path, "producer.har"), + format: "har", + sensitive_values: [literal], + }); + assert.equal(value.container.records_pointer, null); + assert.ok( + value.records.every( + (record) => + record.location.kind === "unknown" && + record.location.reason === "explicit-sensitive-value", + ), + ); + assert.ok(!JSON.stringify(value).includes(literal)); + cases++; + } + for (const format of ["har", "mitmproxy"]) { + const value = await inspect(mode, { + capture_path: join( + runtime.path, + format === "har" ? "producer.har" : "string-urls.mitm", + ), + format, + sensitive_values: ["~1"], + }); + assert.ok(!JSON.stringify(value).includes("~1")); + cases++; + } + } + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + for (const sensitive_values of [ + ["producer"], + ["producer", "REDACTED", "…"], + ]) { + await inspect(mode, { + capture_path: join( + runtime.path, + format === "har" ? "producer.har" : "flows.mitm", + ), + format, + sensitive_values, + }); + cases++; + } + } + } + for (const mode of ["cli", "mcp"]) { + for (const selection of [ + { record_ordinals: [12345], sensitive_values: ["12345"] }, + { record_ordinals: [0, 0], sensitive_values: ["Each"] }, + ]) { + await inspect( + mode, + { + capture_path: join(runtime.path, "producer.har"), + format: "har", + ...selection, + }, + "invalid_input", + ); + cases++; + } + } + await inspect( + "mcp", + { + capture_path: "relative.har", + format: "har", + sensitive_values: ["absolute"], + }, + "invalid_input", + ); + cases++; + const privatePath = join(runtime.path, "REDACTED.har"); + for (const name of ["mysecret", "/ordinary/secret"]) { + const unknownArgument = await inspect( + "mcp", + { + capture_path: join(runtime.path, "producer.har"), + format: "har", + sensitive_values: ["secret"], + [name]: true, + }, + "invalid_input", + ); + assert.deepEqual(unknownArgument.details.issues[0].path, []); + cases++; + } + await inspect("mcp", { + capture_path: join(runtime.path, "flows.mitm"), + format: "mitmproxy", + sensitive_values: ["\ud800", "ordinary-scalar"], + }); + cases++; + const replacementHar = JSON.parse( + await readFile(join(runtime.path, "producer.har"), "utf8"), + ); + replacementHar.log.entries[0].response.content = { + size: 3, + mimeType: "application/octet-stream", + encoding: "base64", + text: "77+9", + }; + const replacementHarPath = join(runtime.path, "unicode-scalar.har"); + await writeFile(replacementHarPath, JSON.stringify(replacementHar)); + for (const literal of ["\ud800", "\udfff", "\ufffd"]) { + const unicode = await inspect("mcp", { + capture_path: replacementHarPath, + format: "har", + sensitive_values: [literal], + }); + const body = unicode.records[0].binary_fields.find( + (field) => field.pointer === "/response/content/text", + ); + assert.equal(body?.state, literal === "\ufffd" ? "redacted" : "retained"); + assert.equal(body.content_base64, literal === "\ufffd" ? null : "77+9"); + cases++; + } + const numericHar = ( + await readFile(join(runtime.path, "producer.har"), "utf8") + ).replace('"cache": {}', '"cache": {}, "_big": 9007199254740993'); + const numericPath = join(runtime.path, "numeric.har"); + await writeFile(numericPath, numericHar); + assert.ok(numericHar.includes('"_big": 9007199254740993')); + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + for (const literal of ["9007199254740993", "4740993"]) { + const value = await inspect(mode, { + capture_path: + format === "har" ? numericPath : join(runtime.path, "flows.mitm"), + format, + sensitive_values: [literal], + }); + assert.ok(!JSON.stringify(value).includes(literal)); + const first = value.records[0]; + const pointer = format === "har" ? "/_big" : "/metadata/big_integer"; + assert.ok( + first.redactions.some( + (redaction) => + redaction.pointer === pointer && + redaction.reason === "explicit-sensitive-value", + ), + ); + assert.ok( + first.numeric_literals.every( + (number) => !number.literal.includes(literal), + ), + ); + cases++; + } + } + } + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + await inspect(mode, { + capture_path: join( + runtime.path, + format === "har" ? "producer.har" : "flows.mitm", + ), + format, + sensitive_values: [format], + }); + cases++; + } + } + const encodedHar = JSON.parse( + await readFile(join(runtime.path, "producer.har"), "utf8"), + ); + encodedHar.log.entries[0].response.content = { + size: 6, + mimeType: "application/octet-stream", + encoding: "base64", + text: "c2 VjcmV0", + }; + const encodedHarPath = join(runtime.path, "encoded-sensitive.har"); + await writeFile(encodedHarPath, JSON.stringify(encodedHar)); + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + const bytes = + format === "har" + ? Buffer.from("secret") + : Buffer.from([0, 255, ...Buffer.from("body")]); + for (const literal of [ + bytes.toString("base64"), + bytes.toString("base64").slice(0, 4), + createHash("sha256").update(bytes).digest("hex"), + ]) { + const value = await inspect(mode, { + capture_path: + format === "har" + ? encodedHarPath + : join(runtime.path, "flows.mitm"), + format, + sensitive_values: [literal], + }); + const pointer = + format === "har" ? "/response/content/text" : "/request/content"; + const field = value.records[0].binary_fields.find( + (field) => field.pointer === pointer, + ); + assert.equal(field?.state, "redacted"); + assert.equal(field.content_base64, null); + assert.equal(field.sha256, null); + assert.ok(!JSON.stringify(value).includes(literal)); + cases++; + } + } + } + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + const capturePath = join( + runtime.path, + format === "har" ? "producer.har" : "flows.mitm", + ); + const digest = createHash("sha256") + .update(await readFile(capturePath)) + .digest("hex"); + for (const literal of [digest, digest.slice(0, 12)]) { + const value = await inspect(mode, { + capture_path: capturePath, + format, + sensitive_values: [literal], + }); + assert.equal(value.artifact.sha256, null); + assert.equal(value.artifact.path, capturePath); + assert.ok(!JSON.stringify(value).includes(literal)); + cases++; + } + } + } + const oversizedPath = join(runtime.path, "oversized-capture"); + for (const mode of ["cli", "mcp"]) { + for (const literal of ["log", "entries"]) { + const hidden = await inspect(mode, { + capture_path: join(runtime.path, "producer.har"), + format: "har", + sensitive_values: [literal], + }); + assert.equal(hidden.total_records, 2); + assert.equal(hidden.container.records_pointer, null); + for (const [ordinal, record] of hidden.records.entries()) { + assert.equal(record.ordinal, ordinal); + assert.deepEqual(record.location, { + kind: "unknown", + reason: "explicit-sensitive-value", + }); + assert.equal(record.reported, null); + assert.deepEqual(record.binary_fields, []); + assert.deepEqual(record.numeric_literals, []); + } + cases++; + } + } + await writeFile(oversizedPath, ""); + await truncate(oversizedPath, WEB_NETWORK_CAPTURE_LIMITS.inputBytes + 1); + await copyFile(join(runtime.path, "producer.har"), privatePath); + for (const mode of ["cli", "mcp"]) { + for (const format of ["har", "mitmproxy"]) { + const failure = await inspect( + mode, + { capture_path: oversizedPath, format }, + "invalid_input", + ); + assert.equal(failure.details.issues[0].reason, "out_of_range"); + assert.deepEqual(failure.details.issues[0].path, ["capture_path"]); + assert.equal( + failure.details.issues[0].expected.maximum_capture_bytes, + WEB_NETWORK_CAPTURE_LIMITS.inputBytes, + ); + cases++; + } + for (const format of ["har", "mitmproxy"]) { + const failure = await inspect( + mode, + { + capture_path: join( + runtime.path, + format === "har" ? "deep.har" : "deep.mitm", + ), + format, + }, + "invalid_input", + ); + const issue = failure.details.issues[0]; + assert.equal(issue.reason, "out_of_range"); + assert.equal(issue.path[0], "capture_path"); + assert.match(issue.path[1], /^\/_extension\/child/); + assert.equal( + issue.expected.maximum_capture_nesting, + WEB_NETWORK_CAPTURE_LIMITS.depth, + ); + cases++; + } + await inspect( + mode, + { + capture_path: join(runtime.path, "invalid-credential-key.mitm"), + format: "mitmproxy", + }, + "invalid_input", + ); + cases++; + const credentialDepth = await inspect( + mode, + { + capture_path: join(runtime.path, "deep-credential.mitm"), + format: "mitmproxy", + }, + "invalid_input", + ); + assert.equal(credentialDepth.details.issues[0].reason, "out_of_range"); + assert.match( + credentialDepth.details.issues[0].path[1], + /^\/request\/headers\/0\/1\/child/, + ); + cases++; + const value = await inspect(mode, { + capture_path: privatePath, + format: "har", + sensitive_values: ["REDACTED", "["], + }); + assert.equal(value.artifact.path, ""); + assert.ok(!JSON.stringify(value).includes("REDACTED")); + cases++; + await inspect( + mode, + { + capture_path: join(runtime.path, "invalid-private-parent.har"), + format: "har", + sensitive_values: ["response"], + }, + "invalid_input", + ); + cases++; + await inspect( + mode, + { + capture_path: join(runtime.path, "private-property-absent.har"), + format: "har", + sensitive_values: ["private-property", "REDACTED"], + }, + "invalid_input", + ); + cases++; + await inspect( + mode, + { + capture_path: join(runtime.path, "invalid-private-key.mitm"), + format: "mitmproxy", + sensitive_values: ["private-property"], + }, + "invalid_input", + ); + cases++; + } + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { + capture_path: join(runtime.path, "flows.mitm"), + format: "mitmproxy", + record_ordinals: [0], + sensitive_values: ["body"], + }); + assert.equal( + value.records[0].binary_fields.find( + (field) => field.pointer === "/request/content", + ).state, + "redacted", + ); + cases++; + } + for (const format of ["har", "mitmproxy"]) { + const path = join(runtime.path, `empty-${format}`); + await writeFile( + path, + format === "har" + ? JSON.stringify({ + log: { + version: "1.2", + creator: { name: "source-owned-empty-fixture", version: "1" }, + entries: [], + }, + }) + : "", + ); + for (const mode of ["cli", "mcp"]) { + const value = await inspect(mode, { capture_path: path, format }); + assert.equal(value.total_records, 0); + cases++; + } + } +} catch (cause) { + failures.push(cause); +} finally { + for (const cleanup of [ + () => client.close(), + () => transport.close(), + () => runtime.close(), + ]) { + try { + await cleanup(); + } catch (cause) { + failures.push(cause); + } + } +} +const verifier = await completeVerifierRun(run); +try { + assert.equal(verifier.process_lineage.status, "verified"); + assert.deepEqual(verifier.process_lineage.descendants, []); +} catch (cause) { + failures.push(cause); +} +if (failures.length > 0) { + console.error( + JSON.stringify( + { status: "failed", public_cases: cases, verifier }, + null, + 2, + ), + ); + throw new AggregateError( + failures, + "Historical capture verification failed; analysis and cleanup failures are retained.", + ); +} +console.log( + JSON.stringify( + { + status: "passed", + public_cases: cases, + upstream: "mitmproxy 12.2.3 FlowWriter + SaveHar", + native_failure_classifications: 7, + offline: true, + verifier, + }, + null, + 2, + ), +); + +async function inspect(mode, input, errorCategory) { + if (mode === "mcp") { + const result = await client.callTool({ + name: "inspect_web_network_capture", + arguments: input, + }); + const value = JSON.parse(mcpTextValue(result)); + if (errorCategory !== undefined) { + assert.equal(result.isError, true); + assert.equal(value.error.category, errorCategory); + for (const literal of input.sensitive_values ?? []) + assert.ok(!JSON.stringify(value.error).includes(literal)); + return value.error; + } + assert.notEqual(result.isError, true, mcpTextValue(result)); + assertSensitiveLimitations(value.evidence, input.sensitive_values ?? []); + return value.result; + } + const args = [ + entrypoint, + "inspect-web-network-capture", + input.capture_path, + input.format, + "--json", + ...(input.record_ordinals ?? []).flatMap((ordinal) => [ + "--record", + String(ordinal), + ]), + ...(input.sensitive_values ?? []).flatMap((value) => [ + "--sensitive-value", + value, + ]), + ]; + let result; + try { + result = await promisify(execFile)(process.execPath, args, { + env: environment, + timeout: 40_000, + maxBuffer: 96 * 1024 * 1024, + }); + } catch (cause) { + if (errorCategory === undefined) throw cause; + assert.equal(typeof cause.code, "number"); + const value = JSON.parse(cause.stdout); + assert.equal(value.category, errorCategory); + for (const literal of input.sensitive_values ?? []) + assert.ok(!JSON.stringify(value).includes(literal)); + return value; + } + assert.equal(errorCategory, undefined, "Expected malformed capture to fail"); + const evidence = JSON.parse(result.stdout); + assertSensitiveLimitations(evidence, input.sensitive_values ?? []); + return evidence.normalized_result; +} + +function assertSensitiveLimitations(evidence, literals) { + for (const value of Object.values(evidence.parameters)) + if (typeof value === "string") + for (const literal of literals) + assert.ok( + !value.includes(literal), + "Declared literal retained in Evidence parameter", + ); + const result = evidence.normalized_result; + if (result.artifact.sha256 === null) assert.equal(evidence.subject, null); + else assert.equal(evidence.subject.digest.sha256, result.artifact.sha256); + assert.deepEqual( + evidence.provider, + result.decoder, + "Evidence provider tuple changed", + ); + assert.equal( + result.decoder.name, + result.format === "har" + ? "REA HAR capture adapter" + : "REA offline native mitmproxy adapter", + ); + const texts = [ + evidence.limitations, + result.limitations, + ...result.records.map((record) => record.limitations), + ].flat(); + for (const literal of literals) + assert.ok( + texts.every((text) => !text.includes(literal)), + "Declared literal retained in authored limitations", + ); +} + +function assertBinary(record, pointer, bytes) { + const field = record.binary_fields.find((item) => item.pointer === pointer); + assert.ok(field, `Missing native byte field ${pointer}`); + assert.equal(field.state, "retained"); + assert.equal(field.content_base64, bytes.toString("base64")); + assert.equal(field.bytes, bytes.length); + assert.equal(field.sha256, createHash("sha256").update(bytes).digest("hex")); +} diff --git a/scripts/verify-windows-native.mjs b/scripts/verify-windows-native.mjs index eb079dedba..7b95c8e4a7 100644 --- a/scripts/verify-windows-native.mjs +++ b/scripts/verify-windows-native.mjs @@ -263,6 +263,80 @@ try { ]); native.call("runtime_snapshot_cancel", [runtime.handle]); await assert.rejects(cancelled, /cancelled/u); + assert.equal(await exists(join(runtime.path, "cancelled.bin")), false); + const readbackRuntime = WindowsPrivateRuntime.create( + workspace.replaceAll("\\", "/"), + "readback-", + ); + try { + readbackRuntime.writeFile("written.txt", "runtime-write-readback"); + assert.equal( + readbackRuntime.readFile("written.txt"), + "runtime-write-readback", + ); + const readbackSnapshot = await readbackRuntime.snapshot( + source, + "snapshot.bin", + ); + assert.equal( + readbackSnapshot.sha256, + createHash("sha256") + .update(await readFile(source)) + .digest("hex"), + ); + assert.equal( + readbackRuntime.readFile("snapshot.bin"), + await readFile(source, "utf8"), + ); + const writtenPath = join(readbackRuntime.observation.path, "written.txt"); + await assert.rejects(writeFile(writtenPath, "replace")); + await assert.rejects(rename(writtenPath, `${writtenPath}.renamed`)); + await assert.rejects(rm(writtenPath)); + assert.equal( + readbackRuntime.readFile("written.txt"), + "runtime-write-readback", + ); + const snapshotPath = join( + readbackRuntime.observation.path, + "snapshot.bin", + ); + // Java RandomAccessFile uses read/write sharing without delete sharing. + // Exercise that independent Win32 reader contract without requiring Java. + const readerDigest = execFileSync( + "powershell.exe", + [ + "-NoProfile", + "-NonInteractive", + "-Command", + "$ErrorActionPreference = 'Stop'; " + + "$stream = [IO.File]::Open($env:REA_SNAPSHOT_READER_PATH, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite); " + + "$hash = [Security.Cryptography.SHA256]::Create(); " + + "try { [BitConverter]::ToString($hash.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() } " + + "finally { $hash.Dispose(); $stream.Dispose() }", + ], + { + encoding: "utf8", + timeout: 15_000, + env: { ...process.env, REA_SNAPSHOT_READER_PATH: snapshotPath }, + }, + ).trim(); + assert.equal(readerDigest, readbackSnapshot.sha256); + await assert.rejects(writeFile(snapshotPath, "replace")); + await assert.rejects(rename(snapshotPath, `${snapshotPath}.renamed`)); + await assert.rejects(rm(snapshotPath)); + assert.equal( + readbackRuntime.readFile("snapshot.bin"), + await readFile(source, "utf8"), + ); + report.controls = { + ...report.controls, + completedRuntimeFileReadback: true, + completedRuntimeFileMutationDenied: true, + snapshotReadWriteSharingReader: true, + }; + } finally { + await readbackRuntime.close(); + } report.controls = { ...report.controls, protectedDaclReadback: true, diff --git a/scripts/verify/apple/macos-bundle.mjs b/scripts/verify/apple/macos-bundle.mjs index bc4522b731..972c8a7c4a 100644 --- a/scripts/verify/apple/macos-bundle.mjs +++ b/scripts/verify/apple/macos-bundle.mjs @@ -14,6 +14,7 @@ import { buildMacosBundleFixture, preflightMacosBundleFixture, } from "../../fixtures/apple/macos-bundle.mjs"; +import { verifyDylibResolution } from "./macos-dylib-e2e.mjs"; const exec = promisify(execFile); @@ -150,6 +151,7 @@ try { assert.deepEqual(anatomies[2], anatomies[0], "DMG anatomy differs"); const { stdout: attached } = await exec("/usr/bin/hdiutil", ["info"]); assert.ok(!attached.includes(dmg), "DMG remained attached after inventory"); + const dylibs = await verifyDylibResolution(app); process.stdout.write( `${JSON.stringify({ @@ -160,6 +162,7 @@ try { containers: containers.map(({ format }) => format), bundles: EXPECTED_BUNDLES.length, dmg_detached: true, + dylib_resolution: dylibs, })}\n`, ); } finally { diff --git a/scripts/verify/apple/macos-dylib-e2e.mjs b/scripts/verify/apple/macos-dylib-e2e.mjs new file mode 100644 index 0000000000..eacd06e9fb --- /dev/null +++ b/scripts/verify/apple/macos-dylib-e2e.mjs @@ -0,0 +1,209 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { realpath } from "node:fs/promises"; +import { join, relative } from "node:path"; +import { promisify } from "node:util"; + +import { + artifactCliEvidence, + artifactMcpResult, + withArtifactMcp, +} from "../../lib/artifact-e2e.mjs"; + +const exec = promisify(execFile); + +const MAIN = "Contents/MacOS/MacFixture"; +const SERVICE = "Contents/XPCServices/Svc.xpc/Contents/MacOS/Svc"; +const CORE = "Contents/Frameworks/Core.framework/Versions/A/Core"; +const DEPENDENCY_COMMANDS = new Set([ + "LC_LOAD_DYLIB", + "LC_LOAD_WEAK_DYLIB", + "LC_REEXPORT_DYLIB", + "LC_LAZY_LOAD_DYLIB", + "LC_LOAD_UPWARD_DYLIB", +]); + +/** Dependencies, rpaths and install name of one slice as Apple's otool reports them. */ +const otoolLoadCommands = async (path, architecture) => { + const { stdout } = await exec( + "/usr/bin/xcrun", + ["otool", "-arch", architecture, "-l", path], + { maxBuffer: 64 * 1024 * 1024 }, + ); + const result = { dependencies: [], rpaths: [], install_name: null }; + for (const block of stdout.split(/\nLoad command \d+\n/u)) { + const command = /^\s*cmd (\S+)/mu.exec(block)?.[1]; + const name = /^\s*name (.+?) \(offset \d+\)$/mu.exec(block)?.[1]; + if (command === "LC_RPATH") + result.rpaths.push(/^\s*path (.+?) \(offset \d+\)$/mu.exec(block)?.[1]); + else if (command === "LC_ID_DYLIB") result.install_name = name; + else if (DEPENDENCY_COMMANDS.has(command)) + result.dependencies.push(`${command} ${name}`); + } + return result; +}; + +/** In-bundle images dyld actually loaded, in load order. */ +const loadedImages = async (app, executable) => { + const { stderr } = await exec(join(app, executable), [], { + env: { PATH: "/usr/bin:/bin", DYLD_PRINT_LIBRARIES: "1" }, + }); + const appRoot = await realpath(app); + const loaded = []; + for (const line of stderr.split("\n")) { + const path = /^dyld\[\d+\]: <[0-9A-F-]+> (.+)$/u.exec(line)?.[1]; + if (path === undefined) continue; + // Shared-cache libraries are reported by path but are not files on disk. + const real = await realpath(path).catch(() => undefined); + if (real?.startsWith(`${appRoot}/`) === true) + loaded.push(relative(appRoot, real)); + } + return loaded; +}; + +/** Images a process root loads according to the trace, in first-load order. */ +const predictedImages = (trace, root) => { + const loaded = [root]; + for (const edge of trace.edges) + if ( + edge.root === root && + ["resolved", "conditional"].includes(edge.resolution.status) && + !loaded.includes(edge.resolution.image) + ) + loaded.push(edge.resolution.image); + return loaded; +}; + +const findEdge = (trace, root, loader, installName) => { + const edge = trace.edges.find( + (candidate) => + candidate.root === root && + candidate.loader === loader && + candidate.install_name === installName, + ); + assert.ok(edge, `missing edge ${root}: ${loader} -> ${installName}`); + return edge; +}; + +const verifyResolutionShapes = (trace) => { + const core = findEdge( + trace, + MAIN, + MAIN, + "@rpath/Core.framework/Versions/A/Core", + ); + assert.deepEqual( + core.candidates.map(({ source, outcome }) => [source, outcome]), + [ + ["rpath", "absent"], + ["rpath", "resolved"], + ], + ); + assert.deepEqual(core.resolution, { status: "resolved", image: CORE }); + const weak = findEdge(trace, MAIN, MAIN, "@rpath/libgone.dylib"); + assert.equal(weak.weak, true); + assert.equal(weak.resolution.status, "unresolved"); + const delayed = findEdge(trace, MAIN, MAIN, "@rpath/libdelay.dylib"); + assert.equal(delayed.encoding, "dylib_use_command"); + assert.equal(delayed.delayed_init, true); + const chain = findEdge(trace, MAIN, CORE, "@rpath/libchain.dylib"); + assert.equal( + chain.resolution.image, + "Contents/Frameworks/Core.framework/Versions/A/Libraries/libchain.dylib", + "the loading image's own LC_RPATH must be searched first", + ); + const service = findEdge( + trace, + SERVICE, + SERVICE, + "@rpath/Core.framework/Versions/A/Core", + ); + assert.equal(service.candidates[0]?.outcome, "outside-target"); + assert.deepEqual(service.resolution, { status: "conditional", image: CORE }); + const system = trace.edges.filter(({ install_name: name }) => + name.startsWith("/usr/lib/"), + ); + assert.ok(system.length > 0); + assert.ok( + system.every(({ resolution }) => resolution.status === "undetermined"), + ); + const kinds = trace.findings.map(({ kind, image }) => `${kind}:${image}`); + assert.ok(kinds.includes(`weak-load-unresolved:${MAIN}`)); + assert.ok(kinds.includes(`earlier-rpath-candidate-absent:${MAIN}`)); +}; + +const verifyParserConformance = async (app, trace) => { + let slices = 0; + for (const image of trace.images) + for (const slice of image.slices) { + const expected = await otoolLoadCommands( + join(app, image.path), + slice.architecture, + ); + const edges = trace.edges.filter( + ({ loader, architecture, root }) => + loader === image.path && + architecture === slice.architecture && + root === + trace.edges.find( + (edge) => + edge.loader === image.path && + edge.architecture === slice.architecture, + )?.root, + ); + assert.deepEqual( + { + dependencies: edges.map( + ({ command, install_name: name }) => `${command} ${name}`, + ), + rpaths: slice.rpaths, + install_name: slice.install_name, + }, + expected, + `${image.path} (${slice.architecture}) differs from otool -l`, + ); + slices += 1; + } + return slices; +}; + +/** Verify trace_dylib_resolution against otool and the real dyld on the fixture app. */ +export async function verifyDylibResolution(app) { + const trace = (await artifactCliEvidence("trace-dylib-resolution", app)) + .normalized_result; + await withArtifactMcp(app, async (client) => { + assert.deepEqual( + await artifactMcpResult(client, "trace_dylib_resolution"), + trace, + ); + const invalid = await client.callTool({ + name: "trace_dylib_resolution", + arguments: { roots: ["../outside"] }, + }); + assert.equal(invalid.isError, true); + }); + assert.equal(trace.coverage.status, "complete"); + assert.deepEqual(trace.roots.map(({ image }) => image).sort(), [ + "Contents/Frameworks/Core.framework/Versions/A/XPCServices/FwSvc.xpc/Contents/MacOS/FwSvc", + "Contents/Helpers/rea-tool", + "Contents/Library/LaunchServices/com.example.rea.helper", + "Contents/Library/LoginItems/Login.app/Contents/MacOS/Login", + MAIN, + "Contents/PlugIns/Ext.appex/Contents/MacOS/Ext", + SERVICE, + ]); + verifyResolutionShapes(trace); + const slices = await verifyParserConformance(app, trace); + for (const root of [MAIN, SERVICE]) + assert.deepEqual( + await loadedImages(app, root), + predictedImages(trace, root), + `${root} load order differs from DYLD_PRINT_LIBRARIES`, + ); + return { + roots: trace.roots.length, + edges: trace.edges.length, + otool_slices: slices, + dyld_runtime_roots: 2, + }; +} diff --git a/scripts/verify/javascript/output.mjs b/scripts/verify/javascript/output.mjs new file mode 100644 index 0000000000..0e6d5635ea --- /dev/null +++ b/scripts/verify/javascript/output.mjs @@ -0,0 +1,134 @@ +import assert from "node:assert/strict"; +import { constants } from "node:buffer"; +import { createHash } from "node:crypto"; +import { once } from "node:events"; +import { createReadStream, createWriteStream } from "node:fs"; +import { mkdtemp, rm, stat, statfs } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { finished } from "node:stream/promises"; + +import { Cli } from "incur"; + +import { createStreamedCliJsonOutput } from "../../../dist/cli/streamedJsonOutput.js"; +import { createEvidence } from "../../../dist/domain/evidence.js"; + +// This opt-in format-boundary check writes one temporary file larger than the +// actual engine string limit. Shared leaves keep the analysis input small. +const format = process.argv[2] ?? "json"; +assert.ok(format === "json" || format === "jsonl", "Use json or jsonl"); +const leaf = "x".repeat(64 * 1024); +const encodedLeaf = JSON.stringify(leaf); +const count = Math.ceil(constants.MAX_STRING_LENGTH / (leaf.length + 3)) + 1; +const volume = await statfs(tmpdir()); +assert.ok( + volume.bavail * volume.bsize > count * encodedLeaf.length + 1024 ** 3, + "Large-output verification needs room for one output file and a 1-GiB reserve", +); +const workspace = await mkdtemp(join(tmpdir(), "rea-javascript-output-")); +const path = join(workspace, "result.json"); +let destination; +let peakRss = process.memoryUsage().rss; +const sampleRss = () => { + peakRss = Math.max(peakRss, process.memoryUsage().rss); +}; +const sampler = setInterval(sampleRss, 25); +sampler.unref(); + +try { + const evidence = createEvidence( + undefined, + { id: "large-output-fixture", name: "Large output fixture", version: "1" }, + { + operation: "inspect_fixture", + parameters: {}, + result: { payload: Array.from({ length: count }, () => leaf) }, + }, + ); + sampleRss(); + const metadata = { command: "inspect-output-fixture", duration: "0ms" }; + const marker = "__REA_OUTPUT_ENCODING_MARKER__"; + const template = { + ok: true, + data: { ...evidence, normalized_result: { payload: [marker] } }, + meta: metadata, + }; + const skeleton = JSON.stringify(template, null, format === "json" ? 2 : 0); + const encodedMarker = JSON.stringify(marker); + const markerIndex = skeleton.indexOf(encodedMarker); + assert.ok(markerIndex >= 0); + assert.equal(markerIndex, skeleton.lastIndexOf(encodedMarker)); + const prefix = skeleton.slice(0, markerIndex); + const suffix = `${skeleton.slice(markerIndex + encodedMarker.length)}\n`; + const indentation = prefix.slice(prefix.lastIndexOf("\n") + 1); + const separator = format === "json" ? `,\n${indentation}` : ","; + const expectedHash = createHash("sha256"); + expectedHash.update(prefix); + for (let index = 0; index < count; index += 1) { + if (index > 0) expectedHash.update(separator); + expectedHash.update(encodedLeaf); + } + expectedHash.update(suffix); + const expectedBytes = + Buffer.byteLength(prefix) + + count * Buffer.byteLength(encodedLeaf) + + (count - 1) * Buffer.byteLength(separator) + + Buffer.byteLength(suffix); + assert.ok(expectedBytes > constants.MAX_STRING_LENGTH); + + destination = createWriteStream(path, { flags: "wx", mode: 0o600 }); + const argv = [metadata.command, "--format", format, "--full-output"]; + const output = createStreamedCliJsonOutput(argv, destination); + assert.ok(output); + let unexpectedOutputBytes = 0; + let exitCode = 0; + await Cli.create("rea-output-verifier", { sync: false }) + .command(metadata.command, { + run: async ({ format: selectedFormat }) => { + assert.equal( + await output.write(evidence, { ...metadata, format: selectedFormat }), + true, + ); + }, + }) + .serve(argv, { + stdout: (text) => { + if (!output.handled) unexpectedOutputBytes += Buffer.byteLength(text); + }, + exit: (code) => { + exitCode = code; + }, + }); + destination.end(); + await finished(destination); + assert.equal(exitCode, 0); + assert.equal(output.handled, true); + assert.equal(output.failed, false); + assert.equal(unexpectedOutputBytes, 0); + + const actualHash = createHash("sha256"); + for await (const chunk of createReadStream(path)) actualHash.update(chunk); + const digest = actualHash.digest("hex"); + assert.equal(digest, expectedHash.digest("hex")); + assert.equal((await stat(path)).size, expectedBytes); + sampleRss(); + process.stdout.write( + `${JSON.stringify({ + json_output_verified: true, + format, + output_bytes: expectedBytes, + engine_string_limit: constants.MAX_STRING_LENGTH, + output_sha256: digest, + evidence_id: evidence.evidence_id, + rss: { method: "node-memoryUsage-rss-sampled", peak_bytes: peakRss }, + })}\n`, + ); +} finally { + clearInterval(sampler); + if (destination !== undefined && !destination.closed) { + const closed = once(destination, "close"); + destination.destroy(); + await closed; + } + await rm(workspace, { recursive: true, force: true }); +} diff --git a/scripts/verify/recorded-crash/fixtures.mjs b/scripts/verify/recorded-crash/fixtures.mjs new file mode 100644 index 0000000000..2ad46803f6 --- /dev/null +++ b/scripts/verify/recorded-crash/fixtures.mjs @@ -0,0 +1,142 @@ +import assert from "node:assert/strict"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; +import { runOwnedCommand } from "../../../dist/process/OwnedCommand.js"; +import { + spawnOwnedProviderProcess, + ProviderProcessSupervisor, +} from "../../../dist/process/ProviderProcess.js"; +import { cleanupOwnedProcessGroup } from "../../../dist/process/ProcessOwnership.js"; + +/** Execute fixture tooling under an owned deadline; fixture generation deliberately runs its own test program. */ +export async function fixtureCommand( + command, + arguments_, + { root, environment, runId }, +) { + const result = await runOwnedCommand( + { + command, + arguments: arguments_, + cwd: root, + runId, + hostEnvironment: environment, + expectedCommand: null, + }, + { timeoutMs: 45_000, diagnosticBytes: 1024 * 1024 }, + ); + return result.stdout.text; +} + +/** Build and record a source-owned two-thread crash without changing kernel core settings. */ +export async function createRecordedCore(options) { + const executable = join(options.root, "fixture-program"); + const core = join(options.root, "recording.core"); + await fixtureCommand( + "gcc", + [ + "-g", + "-O0", + "-pthread", + "-fno-pie", + "-no-pie", + fileURLToPath( + new URL("../../fixtures/recorded-crash.c", import.meta.url), + ), + "-o", + executable, + ], + options, + ); + // Fixed relative arguments avoid putting selected filesystem paths in GDB command syntax. + await fixtureCommand( + options.environment.REA_PWNDBG_GDB, + [ + "-nx", + "-nh", + "--batch", + "--quiet", + "-iex", + "set auto-load off", + "-iex", + "set debuginfod enabled off", + "-ex", + "set startup-with-shell off", + "-ex", + "set disable-randomization off", + "-ex", + "file ./fixture-program", + "-ex", + "run", + "-ex", + "generate-core-file ./recording.core", + ], + options, + ); + return core; +} + +/** Retain authority over an unrelated live process used only as a historical-PID collision oracle. */ +export async function startCoreSentinel(options) { + const launch = await spawnOwnedProviderProcess({ + command: process.execPath, + arguments: [ + "-e", + "process.stdout.write('ready\\n'); setInterval(() => {}, 1000)", + ], + cwd: options.root, + hostEnvironment: options.environment, + runId: options.runId, + }); + const supervisor = new ProviderProcessSupervisor({ + ...launch, + ownsProcessLifetime: true, + cleanup: () => cleanupOwnedProcessGroup(launch.ownership), + }); + try { + await new Promise((resolve, reject) => { + const timer = setTimeout( + () => finish(new Error("Sentinel readiness timed out")), + 10_000, + ); + const onData = () => { + if (supervisor.snapshot().stdout.text.includes("ready\n")) finish(); + }; + const onExit = () => + finish(new Error("Sentinel exited before readiness")); + function finish(error) { + clearTimeout(timer); + launch.process.stdout.off("data", onData); + launch.process.off("exit", onExit); + if (error) reject(error); + else resolve(); + } + launch.process.stdout.on("data", onData); + launch.process.once("exit", onExit); + onData(); + }); + } catch (cause) { + try { + await supervisor.stop(); + } finally { + supervisor.dispose(); + } + throw cause; + } + return { + pid: launch.process.pid, + assertAlive() { + assert.equal(launch.process.exitCode, null); + assert.equal(launch.process.signalCode, null); + process.kill(launch.process.pid, 0); + }, + async close() { + try { + const stopped = await supervisor.stop(); + assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped)); + } finally { + supervisor.dispose(); + } + }, + }; +} diff --git a/scripts/verify/recorded-crash/public.mjs b/scripts/verify/recorded-crash/public.mjs new file mode 100644 index 0000000000..144845df7c --- /dev/null +++ b/scripts/verify/recorded-crash/public.mjs @@ -0,0 +1,133 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { promisify } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { parseEvidence } from "../../../dist/domain/evidence.js"; +import { recordedCrashSchema } from "../../../dist/domain/native/recordedCrash.js"; +import { mcpTextValue } from "../../lib/mcp-verifier-results.mjs"; + +const execute = promisify(execFile); + +/** Exercise the public installed CLI and SDK transport with the same selected environment. */ +export async function connectRecordedCrash({ entrypoint, environment }) { + const client = new Client({ name: "recorded-crash-verifier", version: "1" }); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [entrypoint, "mcp"], + env: environment, + stderr: "pipe", + }); + try { + await client.connect(transport); + } catch (cause) { + await transport.close(); + throw cause; + } + return { + client, + async inspect(mode, path, { debuggerContext = false, category } = {}) { + const original = await readFile(path); + let evidence; + if (mode === "mcp") { + const response = await client.callTool( + { + name: "inspect_recorded_crash", + arguments: { path, include_debugger_context: debuggerContext }, + }, + { timeout: 90_000 }, + ); + const value = JSON.parse(mcpTextValue(response)); + if (category !== undefined) { + assert.equal(response.isError, true); + assert.equal(value.error.category, category, JSON.stringify(value)); + assert.deepEqual(await readFile(path), original); + return value.error; + } + assert.notEqual(response.isError, true, JSON.stringify(value)); + evidence = parseEvidence(value.evidence); + assert.deepEqual(value.result, evidence.normalized_result); + } else { + assert.equal(mode, "cli"); + let response; + try { + response = await execute( + process.execPath, + [ + entrypoint, + "inspect-recorded-crash", + path, + ...(debuggerContext ? ["--debugger-context"] : []), + "--json", + ], + { env: environment, timeout: 90_000, maxBuffer: 32 * 1024 * 1024 }, + ); + } catch (cause) { + if (category === undefined || typeof cause.code !== "number") + throw cause; + const error = JSON.parse(cause.stdout); + assert.equal(error.category, category, JSON.stringify(error)); + assert.deepEqual(await readFile(path), original); + return error; + } + assert.equal(category, undefined, "Expected an explicit failure"); + evidence = parseEvidence(JSON.parse(response.stdout)); + } + const report = recordedCrashSchema.parse(evidence.normalized_result); + assert.equal( + evidence.confidence, + debuggerContext ? "derived" : "observed", + ); + assert.deepEqual(evidence.raw_result, report); + assert.equal(evidence.subject.local_path, path); + assert.equal(report.artifact.path, path); + assert.equal( + report.artifact.sha256, + createHash("sha256").update(original).digest("hex"), + ); + assert.equal(report.artifact.bytes, original.length); + assert.equal( + evidence.provider.version, + "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2", + ); + assert.equal(report.target_execution, "not-performed"); + assert.equal(report.live_process_identity, "unknown"); + assert.equal(report.diagnostics.truncated, false); + assert.equal( + report.debugger.status, + debuggerContext ? "available" : "not-requested", + ); + for (const note of report.notes) { + for (const [encoded, range] of [ + [note.owner_bytes_base64, note.owner_location], + [note.descriptor_bytes_base64, note.descriptor_location], + ]) { + const start = Number(BigInt(range.offset)); + assert.deepEqual( + Buffer.from(encoded, "base64"), + original.subarray(start, start + Number(BigInt(range.bytes))), + ); + } + } + for (const thread of report.threads) { + assert.equal(thread.registers.length, 27); + for (const register of thread.registers) + assert.equal( + BigInt(register.value), + original.readBigUInt64LE(Number(BigInt(register.location.offset))), + ); + } + assert.deepEqual(await readFile(path), original); + return report; + }, + async close() { + try { + await client.close(); + } finally { + await transport.close(); + } + }, + }; +} diff --git a/scripts/verify/recorded-crash/trace-client.mjs b/scripts/verify/recorded-crash/trace-client.mjs new file mode 100644 index 0000000000..f38f1d6cff --- /dev/null +++ b/scripts/verify/recorded-crash/trace-client.mjs @@ -0,0 +1,11 @@ +import { connectRecordedCrash } from "./public.mjs"; + +const session = await connectRecordedCrash({ + entrypoint: process.argv[2], + environment: process.env, +}); +try { + await session.inspect("mcp", process.argv[3], { debuggerContext: true }); +} finally { + await session.close(); +} diff --git a/scripts/verify/recorded-crash/trace.mjs b/scripts/verify/recorded-crash/trace.mjs new file mode 100644 index 0000000000..f5f31624ba --- /dev/null +++ b/scripts/verify/recorded-crash/trace.mjs @@ -0,0 +1,175 @@ +import assert from "node:assert/strict"; +import { readdir, readFile, realpath } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { isAbsolute, join, resolve } from "node:path"; +import { runOwnedCommand } from "../../../dist/process/OwnedCommand.js"; + +/** Check attempted syscalls, including failed attempts, on a recorded PID that collides with an owned live sentinel. */ +export async function verifyCoreInspectionTrace({ + root, + environment, + entrypoint, + core, + sentinel, + runId, +}) { + const gdb = await realpath(environment.REA_PWNDBG_GDB); + const python = await realpath(environment.REA_PWNTOOLS_PYTHON); + const plugin = resolve(environment.REA_PWNDBG_GDBINIT, ".."); + const helperPaths = (name) => + new Set([ + join(environment.REA_PWNDBG_VENV_PATH, "bin", name), + ...(environment.PATH ?? "") + .split(":") + .filter(isAbsolute) + .map((directory) => join(directory, name)), + ]); + const psPaths = helperPaths("ps"); + const iconvPaths = helperPaths("iconv"); + const gitPaths = helperPaths("git"); + const observations = []; + for (const mode of ["cli", "mcp"]) { + sentinel.assertAlive(); + const prefix = join(root, `inspection-${mode}.trace`); + const invocation = + mode === "cli" + ? [ + entrypoint, + "inspect-recorded-crash", + core, + "--debugger-context", + "--json", + ] + : [ + fileURLToPath(new URL("./trace-client.mjs", import.meta.url)), + entrypoint, + core, + ]; + await runOwnedCommand( + { + command: environment.REA_VERIFY_STRACE_COMMAND, + arguments: [ + "-ff", + "-qq", + "-s", + "8192", + "-e", + "trace=%process,%network,%file,ptrace,process_vm_readv,process_vm_writev", + "-o", + prefix, + process.execPath, + ...invocation, + ], + cwd: root, + hostEnvironment: environment, + expectedCommand: null, + runId, + }, + { timeoutMs: 90_000, diagnosticBytes: 1024 * 1024 }, + ); + const executions = []; + const ownershipReads = []; + let gdbObserved = false; + for (const file of await readdir(root)) { + if (!file.startsWith(`inspection-${mode}.trace.`)) continue; + const text = await readFile(join(root, file), "utf8"); + const ownershipTrace = text + .split("\n") + .some( + (line) => + /^execve\("\/(?:usr\/)?bin\/ps",/u.test(line) && + line.includes( + '["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]', + ) && + line.endsWith(" = 0"), + ); + const pidReads = text + .split("\n") + .filter((line) => line.includes(`/proc/${sentinel.pid}/`)); + if (ownershipTrace) { + for (const line of pidReads) { + assert.ok( + new RegExp( + `/proc/${sentinel.pid}/(?:stat|status|cmdline|wchan|environ)"`, + "u", + ).test(line), + `Unexpected ownership observation of the sentinel: ${line}`, + ); + ownershipReads.push(line); + } + } else + assert.deepEqual( + pidReads, + [], + `Historical PID caused a provider lookup in ${file}: ${pidReads.join("\n")}`, + ); + assert.ok( + !/\bptrace\(/u.test(text), + `Inspection attempted ptrace: ${file}`, + ); + assert.ok( + !/\bprocess_vm_(?:readv|writev)\(/u.test(text), + `Inspection attempted process memory access: ${file}`, + ); + assert.ok( + !/\b(?:socket|connect|sendto|sendmsg)\([^\n]*(?:AF_INET|AF_INET6)/u.test( + text, + ), + `Inspection attempted an Internet socket: ${file}`, + ); + for (const line of text.split("\n")) { + if (!/execve(?:at)?\(/u.test(line)) continue; + const match = /^execve\("([^"]+)"/u.exec(line); + assert.notEqual(match, null, `Unresolved executable identity: ${line}`); + assert.ok( + / = (?:0|-1 [A-Z]+)/u.test(line), + `Incomplete execution observation: ${line}`, + ); + const executable = await realpath(match[1]).catch(() => match[1]); + const ownership = + psPaths.has(match[1]) && + line.includes( + '["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]', + ); + const iconv = + iconvPaths.has(match[1]) && line.includes('["iconv", "-l"]'); + const version = + gitPaths.has(match[1]) && + line.includes( + "[" + + [ + "git", + "--git-dir", + join(plugin, ".git"), + "rev-parse", + "--short", + "HEAD", + ] + .map((argument) => JSON.stringify(argument)) + .join(", ") + + "]", + ); + assert.ok( + [process.execPath, python, gdb].includes(executable) || + ownership || + iconv || + version, + `Unexpected attempted host execution: ${line}`, + ); + if (executable === gdb) gdbObserved = true; + executions.push(executable); + } + } + assert.ok(gdbObserved, "Trace must include the actual core-only GDB stage"); + assert.ok(executions.includes(python)); + sentinel.assertAlive(); + observations.push({ + mode, + executions, + historical_pid: sentinel.pid, + sentinel: "still-alive", + ownership_process_table_reads: ownershipReads, + }); + } + return observations; +} diff --git a/server.json b/server.json index 11161d5e7c..a26aea8b0e 100644 --- a/server.json +++ b/server.json @@ -8,13 +8,13 @@ "url": "https://github.com/morluto/rea", "source": "github" }, - "version": "4.1.0", + "version": "6.0.0", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "rea-agents", - "version": "4.1.0", + "version": "6.0.0", "runtimeHint": "npx", "transport": { "type": "stdio" diff --git a/skills/reverse-engineer-anything/SKILL.md b/skill-src/reverse-engineer-anything/SKILL.md similarity index 87% rename from skills/reverse-engineer-anything/SKILL.md rename to skill-src/reverse-engineer-anything/SKILL.md index 202f98db5e..629a727312 100644 --- a/skills/reverse-engineer-anything/SKILL.md +++ b/skill-src/reverse-engineer-anything/SKILL.md @@ -2,9 +2,7 @@ name: reverse-engineer-anything description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis. metadata: - version: "27" - tool_count: 133 - catalog_digest: "63c52e101898418ed77fed2c9c4fc83f743f945581e35637683ffdd282919525" + version: "33" --- # REA @@ -93,7 +91,27 @@ explicit path or endpoint and do not need it. consult the NativeAOT workflow in `references/native-and-artifacts.md` for optional metadata recovery and its supported host/layout boundary. - User-owned browser page already open: `list_browser_targets`. +- Retained HAR/native mitmproxy capture: `inspect_web_network_capture`. Select + original record ordinals when useful; inspect producer fields and byte/number + sidecars without fetching recorded URLs or claiming live attribution. See + [historical captures](https://github.com/morluto/rea/blob/main/docs/web-network-captures.md) for the exact upstream + profile and credential exclusions. - User-owned Electron runtime already open: `list_electron_targets`. +- Explicit local EVM bytecode carrier: `inspect_evm_interface` with caller-selected + `raw` or `hex` encoding. Preserve carrier/decoded digests and treat selectors, + argument strings and mutability as inferred candidates; this performs no chain + lookup or target execution. See the + [EVM bytecode guide](https://github.com/morluto/rea/blob/main/docs/evm-bytecode.md). +- Explicit Linux ELF file for offline layout, symbols, relocations or static + mitigation evidence: `inspect_binary_layout`. This target-free operation uses + caller-supplied pwntools without opening a disassembler database. Preserve its + raw locations and inference/coverage limits; see the + [offline binary guide](https://github.com/morluto/rea/blob/main/docs/binary-diagnostics.md). +- Supplied Linux x86-64 ELF core: `inspect_recorded_crash`. Read every recorded + thread, raw note and source-bound register without opening a live target. + Historical PIDs do not select live processes. Request `include_debugger_context` + only when core-only mapping candidates help; see the + [recorded crash guide](https://github.com/morluto/rea/blob/main/docs/recorded-crashes.md). - Native executable, library, or analysis database: `open_binary`, then use focused analysis tools directly; call `binary_overview` when metadata or inventory context is useful and available from the selected provider. diff --git a/skills/reverse-engineer-anything/references/android-applications.md b/skill-src/reverse-engineer-anything/references/android-applications.md similarity index 100% rename from skills/reverse-engineer-anything/references/android-applications.md rename to skill-src/reverse-engineer-anything/references/android-applications.md diff --git a/skills/reverse-engineer-anything/references/evidence-workflows.md b/skill-src/reverse-engineer-anything/references/evidence-workflows.md similarity index 100% rename from skills/reverse-engineer-anything/references/evidence-workflows.md rename to skill-src/reverse-engineer-anything/references/evidence-workflows.md diff --git a/skills/reverse-engineer-anything/references/javascript-applications.md b/skill-src/reverse-engineer-anything/references/javascript-applications.md similarity index 100% rename from skills/reverse-engineer-anything/references/javascript-applications.md rename to skill-src/reverse-engineer-anything/references/javascript-applications.md diff --git a/skills/reverse-engineer-anything/references/native-and-artifacts.md b/skill-src/reverse-engineer-anything/references/native-and-artifacts.md similarity index 77% rename from skills/reverse-engineer-anything/references/native-and-artifacts.md rename to skill-src/reverse-engineer-anything/references/native-and-artifacts.md index 46a5c7af2e..bf42555e86 100644 --- a/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/skill-src/reverse-engineer-anything/references/native-and-artifacts.md @@ -1,5 +1,21 @@ # Native, managed, and packaged artifacts +## Recorded Linux crashes + +Use `inspect_recorded_crash` with an explicit Linux x86-64 ELF core path to +inspect historical thread registers, signals, raw notes and original file +ranges. It is independent of the active disassembler target. Source-bound +register values and note bytes are observations; missing notes, signal-thread +association and current executable/library identity remain unknown. PIDs are +historical metadata and never authorize live attach or control. + +The optional `include_debugger_context` facet adds mapping candidates through +caller-supplied GDB/pwndbg in an owned core-only session. Display names do not +establish current file identity; zero reported flags leave permissions unknown. +Requested unavailable context fails with setup guidance. See +[recorded crashes](https://github.com/morluto/rea/blob/main/docs/recorded-crashes.md) +for exact upstream profiles, bounds and verification coverage. + ## Native targets After `open_binary`, use focused search, procedure, or function tools directly. @@ -9,6 +25,12 @@ cross-references. Addresses and recovered pseudocode are analysis observations, not original source. Provider unavailability and unsupported metadata remain unknown rather than false. +To see which functions or Objective-C methods a Mach-O actually calls in one +run, use `observe_native_calls` with explicit breakpoints and a bounded +`duration_ms`/`max_events`. It launches a new process under LLDB, so the target +runs and may change files or use the network. Hardened-runtime targets need the +`get-task-allow` entitlement. + Use `binary_session` with no arguments to check the open target, selected provider, and alignment. Its default `result.tool_availability` includes the complete tool inventory with availability reasons and remediation. When choosing @@ -77,6 +99,13 @@ helpers, launchd plists, symlinks, and each bundle's `info_plist_path` and executable candidates. Roles are path conventions, not parsed plists; read the listed plists with `inspect_plist`. +After `open_binary` on a `.app` or Mach-O, use `trace_dylib_resolution` to see +which file each `@rpath`, `@loader_path` and `@executable_path` load reaches for +every executable in the bundle. It also lists missing or weak loads and earlier +`@rpath` candidates that are absent. Narrow large bundles with `roots` or +`architecture`. System paths stay undetermined because the dyld shared cache +provides them. + Use `extract_artifact` when materialized files are needed. It takes no arguments and materializes all regular files into a fresh temporary directory chosen by REA. Symlinks and encrypted entries are inventory facts, not extractable files. diff --git a/skills/reverse-engineer-anything/references/runtime-observation.md b/skill-src/reverse-engineer-anything/references/runtime-observation.md similarity index 100% rename from skills/reverse-engineer-anything/references/runtime-observation.md rename to skill-src/reverse-engineer-anything/references/runtime-observation.md diff --git a/src/android/JadxProvider.ts b/src/android/JadxProvider.ts index 31b43ebed8..9c0be14d93 100644 --- a/src/android/JadxProvider.ts +++ b/src/android/JadxProvider.ts @@ -316,6 +316,7 @@ export class JadxProvider implements AndroidAnalysisPort { ], cwd: root.path, hostEnvironment: this.environment, + signal, }, this.launcher, ); diff --git a/src/application/ArtifactGraphConstruction.test.ts b/src/application/ArtifactGraphConstruction.test.ts deleted file mode 100644 index 1454a504c6..0000000000 --- a/src/application/ArtifactGraphConstruction.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { compareDirectoryChildNames } from "./ArtifactGraphConstruction.js"; - -const tiedNames = ["a.txt", "a\u200b.txt", "a\u200c.txt", "a\u200d.txt"]; - -describe("directory child-name ordering", () => { - it("orders the tied subset lexically with reflexivity and antisymmetry", () => { - for (const left of tiedNames) { - expect(compareDirectoryChildNames(left, left)).toBe(0); - for (const right of tiedNames) { - expect(left.localeCompare(right)).toBe(0); - const compared = compareDirectoryChildNames(left, right); - const lexical = left < right ? -1 : left > right ? 1 : 0; - expect(compared).toBe(lexical); - expect(compared + compareDirectoryChildNames(right, left)).toBe(0); - } - } - }); - - it("is transitive across the tied subset", () => { - for (const first of tiedNames) - for (const second of tiedNames) - for (const third of tiedNames) - if ( - compareDirectoryChildNames(first, second) <= 0 && - compareDirectoryChildNames(second, third) <= 0 - ) - expect( - compareDirectoryChildNames(first, third), - ).toBeLessThanOrEqual(0); - }); - - it.each([ - ["a.txt", "b.txt"], - ["z.txt", "ä.txt"], - ["Z", "a"], - ["a/data.txt", "b/data.txt"], - ])("preserves the current non-tied collation for %s / %s", (left, right) => { - const collated = left.localeCompare(right); - expect(collated).not.toBe(0); - expect(compareDirectoryChildNames(left, right)).toBe(collated); - expect(compareDirectoryChildNames(right, left)).toBe( - right.localeCompare(left), - ); - }); -}); diff --git a/src/application/ArtifactInventoryPolicy.test.ts b/src/application/ArtifactInventoryPolicy.test.ts deleted file mode 100644 index 66c89c0e4e..0000000000 --- a/src/application/ArtifactInventoryPolicy.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { resolveArtifactIntegrityPolicy } from "./ArtifactInventory/policy.js"; - -describe("artifact inventory policy resolution", () => { - it("preserves the selected integrity behavior without disabling verification", () => { - expect(resolveArtifactIntegrityPolicy({ mode: "fail" })).toEqual({ - mode: "fail", - }); - expect( - resolveArtifactIntegrityPolicy({ mode: "record-and-continue" }), - ).toEqual({ mode: "record-and-continue" }); - }); -}); diff --git a/src/application/AuthorizedArtifactInventory.ts b/src/application/AuthorizedArtifactInventory.ts deleted file mode 100644 index 71e2306a07..0000000000 --- a/src/application/AuthorizedArtifactInventory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { realpath } from "node:fs/promises"; - -import { - scanCanonicalArtifactInventory, - type ArtifactInventoryOptions, - type ArtifactInventorySnapshot, -} from "./ArtifactInventory.js"; - -/** Resolve, authorize, and scan one artifact without a second path resolution. */ -export const scanAuthorizedArtifactInventory = async ( - inputPath: string, - options: ArtifactInventoryOptions = {}, -): Promise => { - const path = await realpath(inputPath); - return scanCanonicalArtifactInventory(path, options); -}; diff --git a/src/application/CapabilityInventory.contract.test.ts b/src/application/CapabilityInventory.contract.test.ts deleted file mode 100644 index c9200f405f..0000000000 --- a/src/application/CapabilityInventory.contract.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { toolAvailability } from "../contracts/toolOutputSchemaPrimitives.js"; -import { buildCapabilityInventory } from "./CapabilityInventory.js"; - -describe("capability inventory contract", () => { - it("produces valid availability and rejects contradictory states", () => { - const inventory = buildCapabilityInventory( - { open: false, capabilities: [] }, - { - processCaptureEnabled: false, - }, - ); - const unavailable = inventory.find(({ available }) => !available); - expect(unavailable).toBeDefined(); - expect(toolAvailability.safeParse(unavailable).success).toBe(true); - expect( - toolAvailability.safeParse({ ...unavailable, available: true }).success, - ).toBe(false); - expect( - toolAvailability.safeParse({ - ...unavailable, - available: false, - reason: "available", - }).success, - ).toBe(false); - }); -}); diff --git a/src/application/CapabilityInventory.test.ts b/src/application/CapabilityInventory.test.ts index 2f960f15f1..8ddf4f7244 100644 --- a/src/application/CapabilityInventory.test.ts +++ b/src/application/CapabilityInventory.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import { toolAvailability } from "../contracts/toolOutputSchemaPrimitives.js"; import { buildCapabilityInventory } from "./CapabilityInventory.js"; it("reports module syntax and native resolution availability separately", () => { @@ -249,6 +250,23 @@ describe("capability inventory: caller guidance", () => { } }); + it("keeps the public availability schema consistent with generated states", () => { + const inventory = buildCapabilityInventory(status(), enabledPolicy); + const unavailable = inventory.find(({ available }) => !available); + expect(unavailable).toBeDefined(); + expect(toolAvailability.safeParse(unavailable).success).toBe(true); + expect( + toolAvailability.safeParse({ ...unavailable, available: true }).success, + ).toBe(false); + expect( + toolAvailability.safeParse({ + ...unavailable, + available: false, + reason: "available", + }).success, + ).toBe(false); + }); + it("does not require interactive elicitation for process capture", () => { const withoutElicitation = buildCapabilityInventory( status(), diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index a2dcad5c38..b600de075b 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -31,8 +31,11 @@ type ToolAvailabilityReason = "available" | ToolUnavailabilityReason; type ProviderDescriptor = ProviderCapability; export type AvailabilityPolicy = { + readonly evmInterfaceEnabled?: boolean; readonly processCaptureEnabled: boolean; readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures; + readonly binaryLayoutEnabled?: boolean; + readonly recordedCrashEnabled?: boolean; readonly firmwareInspectionEnabled?: boolean; readonly firmwareExtractionEnabled?: boolean; readonly androidAnalysisEnabled?: boolean; @@ -280,6 +283,30 @@ const workflowAvailabilityFor = ({ remediation: "On Linux x64, provide an absolute REA_WAKARU_COMMAND for Wakaru 1.13.0 and util-linux prlimit. No binary target is required.", }; + if (name === "inspect_binary_layout") + return policy.binaryLayoutEnabled === true + ? { reason: "available", remediation: null } + : { + reason: "provider_missing", + remediation: + "On Linux x64, set absolute REA_PWNTOOLS_PYTHON to caller-supplied Python with pwntools 4.15.0. No active binary target is required.", + }; + if (name === "inspect_evm_interface") + return policy.evmInterfaceEnabled === true + ? { reason: "available", remediation: null } + : { + reason: "provider_missing", + remediation: + "The bundled offline EVM interface profile is currently real-verified on Linux x64. No active binary target or chain endpoint is required.", + }; + if (name === "inspect_recorded_crash") + return policy.recordedCrashEnabled === true + ? { reason: "available", remediation: null } + : { + reason: "provider_missing", + remediation: + "On Linux x64, set absolute REA_PWNTOOLS_PYTHON to caller-supplied Python with pwntools 4.15.0. No active binary target is required.", + }; if (kind === "firmware-provider") { const enabled = name === "inspect_firmware_regions" diff --git a/src/application/ClientRegistrationIdentity.test.ts b/src/application/ClientRegistrationIdentity.test.ts new file mode 100644 index 0000000000..96e8d155c0 --- /dev/null +++ b/src/application/ClientRegistrationIdentity.test.ts @@ -0,0 +1,52 @@ +import { expect, it } from "vitest"; +import { PRODUCT_IDENTITY } from "../identity.js"; +import { + isOwnedClientRegistrationCommand, + npxRegistrationCommand, +} from "./ClientRegistrationIdentity.js"; +import { setupRegistrationCommand } from "./SetupHost.js"; + +it("uses a Windows executable for npm-run setup and preserves POSIX launchers", () => { + expect(setupRegistrationCommand("win32", true)).toEqual([ + "cmd.exe", + "/d", + "/c", + "npx", + "-y", + PRODUCT_IDENTITY.registrationPackageSpecifier, + "mcp", + ]); + for (const platform of ["linux", "darwin"] as const) + expect(setupRegistrationCommand(platform, true)).toEqual([ + "npx", + "-y", + PRODUCT_IDENTITY.registrationPackageSpecifier, + "mcp", + ]); +}); + +it.each( + [ + npxRegistrationCommand("win32"), + ["cmd", "/c", "npx", "-y", "rea-agents@5.0.0", "mcp"], + ["npx", "-y", "rea-agents@5.0.0", "mcp"], + ].map((command) => ({ command })), +)("recognizes owned current and legacy launchers: $command", ({ command }) => { + expect(isOwnedClientRegistrationCommand(command)).toBe(true); +}); + +it.each( + [ + ["cmd.exe", "/d", "/c", "node", "custom.js", "mcp"], + ["cmd.exe", "/k", "npx", "-y", "rea-agents@5.0.0", "mcp"], + ["cmd.exe", "/c", "npx -y rea-agents@5.0.0 mcp"], + ["cmd.exe", "/c", "npx", "-y", "another-package", "mcp"], + ["cmd.exe", "/c", "npx", "-y", "rea-agents@5.0.0", "mcp", "&", "other"], + ["cmd.exe", "/c", "npx", "-y", "rea-agents@5.0.0 & other", "mcp"], + ].map((command) => ({ command })), +)( + "does not claim custom shell commands as REA-owned: $command", + ({ command }) => { + expect(isOwnedClientRegistrationCommand(command)).toBe(false); + }, +); diff --git a/src/application/ClientRegistrationIdentity.ts b/src/application/ClientRegistrationIdentity.ts index 2bc4bd105f..7a887d7c45 100644 --- a/src/application/ClientRegistrationIdentity.ts +++ b/src/application/ClientRegistrationIdentity.ts @@ -3,6 +3,28 @@ import { valid } from "semver"; import { PRODUCT_IDENTITY } from "../identity.js"; +/** Resolve a pinned npx launcher that stdio clients can spawn on the host. */ +export const npxRegistrationCommand = ( + platform: NodeJS.Platform = process.platform, +): readonly string[] => { + const command = [ + "npx", + "-y", + PRODUCT_IDENTITY.registrationPackageSpecifier, + "mcp", + ]; + return platform === "win32" ? ["cmd.exe", "/d", "/c", ...command] : command; +}; + +const unwrapNpxCommand = (command: readonly string[]): readonly string[] => { + const executable = command[0]?.toLowerCase(); + if (executable !== "cmd" && executable !== "cmd.exe") return command; + if (command[1]?.toLowerCase() === "/c") return command.slice(2); + if (command[1]?.toLowerCase() === "/d" && command[2]?.toLowerCase() === "/c") + return command.slice(3); + return []; +}; + /** Check that one parsed command points to REA's MCP entry point. */ export const isOwnedClientRegistrationCommand = ( command: readonly string[], @@ -27,15 +49,16 @@ export const isOwnedClientRegistrationCommand = ( return true; } + const npxCommand = unwrapNpxCommand(command); if ( - command.length !== 4 || - command[0] !== "npx" || - command[1] !== "-y" || - command[3] !== "mcp" + npxCommand.length !== 4 || + npxCommand[0] !== "npx" || + npxCommand[1] !== "-y" || + npxCommand[3] !== "mcp" ) return false; - const packageReference = command[2] ?? ""; + const packageReference = npxCommand[2] ?? ""; if ( packageReference === PRODUCT_IDENTITY.packageName || packageReference === PRODUCT_IDENTITY.packageSpecifier || diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts index 3a820aa8ec..91acc42939 100644 --- a/src/application/ClientRegistrationStatus.ts +++ b/src/application/ClientRegistrationStatus.ts @@ -9,7 +9,10 @@ import { z } from "zod"; import { PRODUCT_IDENTITY } from "../identity.js"; import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js"; -import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; +import { + isOwnedClientRegistrationCommand, + npxRegistrationCommand, +} from "./ClientRegistrationIdentity.js"; import { supportedClients } from "./SupportedClients.js"; import type { SetupClient } from "./SupportedClients.js"; @@ -108,7 +111,12 @@ export const readClientRegistrationStatuses = async ( client.name, client.configPath, command, - registrationAligned(registration, client, currentCommandPath) + registrationAligned( + registration, + client, + currentCommandPath, + options.platform ?? process.platform, + ) ? "aligned" : "stale", ), @@ -132,6 +140,7 @@ const registrationAligned = ( registration: z.output, client: SetupClient, currentCommandPath: string, + platform: NodeJS.Platform, ): boolean => { const command = [registration.command, ...registration.args]; if (registration.disabled === true || registration.enabled === false) @@ -160,12 +169,10 @@ const registrationAligned = ( resolve(command[1] ?? "") === currentCommandPath ) return true; + const npxCommand = npxRegistrationCommand(platform); if ( - command.length === 4 && - command[0] === "npx" && - command[1] === "-y" && - command[2] === PRODUCT_IDENTITY.registrationPackageSpecifier && - command[3] === "mcp" + command.length === npxCommand.length && + command.every((argument, index) => argument === npxCommand[index]) ) return true; return ( diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index a6c27834b5..ea7ed8f6cd 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -5,6 +5,8 @@ import { EnhancedTools } from "./EnhancedTools.js"; import { executeFunctionAnalysisEvidence } from "./FunctionAnalysisEvidence.js"; import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; import type { BinarySession } from "./binary/BinarySession.js"; +import type { SessionProviderRoute } from "./binary/SessionProviderRouter.js"; +import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js"; import { silentLogger, type Logger } from "../logger.js"; import { createEvidence } from "../domain/evidence.js"; import type { Evidence } from "../domain/evidence.js"; @@ -28,10 +30,17 @@ import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js"; -import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; +import { + analysisProfileSchema, + committedProviderSchema, + type AnalysisProfileCommitment, +} from "../domain/analysisProfile.js"; import { err, ok, type Result } from "../domain/result.js"; import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js"; -import type { AnalysisExecution } from "./AnalysisProvider.js"; +import type { + AnalysisExecution, + ProviderIdentity, +} from "./AnalysisProvider.js"; import { REA_WORKFLOW_PROVIDER, workflowAnalysisProfile, @@ -194,7 +203,42 @@ const runAnalysis = async ( }); if (!prepared.ok) return cliError(prepared.error); const { snapshot } = prepared.value; - const opened = await session.open(path, { + let resolvedTarget: ResolvedSessionOpen | undefined; + if (snapshot !== undefined && prepared.value.target !== undefined) { + const preview = await session.previewTarget(prepared.value.target, { + signal, + snapshot, + ...(options.formatHint === undefined + ? {} + : { formatHint: options.formatHint }), + ...(options.providerId === undefined + ? {} + : { providerId: options.providerId }), + }); + if (!preview.ok) return cliError(preview.error); + resolvedTarget = preview.value; + const route = preview.value.route; + const bindingProfile = route.profile ?? undefined; + const evidenceProfile = analysisProfileForRoute(route, tool); + if ( + bindingProfile !== undefined && + evidenceProfile !== undefined && + allowsSnapshotReplay(route, tool) + ) { + const cached = snapshotEvidenceForQuery(snapshot, { + target: preview.value.target, + bindingProfile, + operation: tool, + parameters: arguments_, + provider: isWorkflowEvidenceTool(tool) + ? REA_WORKFLOW_PROVIDER + : providerIdentityForRoute(route, tool), + evidenceProfile, + }); + if (cached !== undefined) return cached; + } + } + const openOptions = { signal, ...(options.formatHint === undefined ? {} @@ -203,26 +247,13 @@ const runAnalysis = async ( ...(options.providerId === undefined ? {} : { providerId: options.providerId }), - }); + }; + const opened = + resolvedTarget === undefined + ? await session.open(path, openOptions) + : await session.openResolvedTarget(resolvedTarget, openOptions); if (!opened.ok) return cliError(opened.error); const evidenceProfile = analysisProfileForEvidence(session, tool); - const bindingProfile = session.analysisProfile(); - if ( - canReplayDirectTool(session, tool) && - snapshot !== undefined && - evidenceProfile !== undefined && - bindingProfile !== undefined - ) { - const cached = snapshotEvidenceForQuery(snapshot, { - target: opened.value, - bindingProfile, - operation: tool, - parameters: arguments_, - provider: replayProviderFor(session, tool), - evidenceProfile, - }); - if (cached !== undefined) return cached; - } const { output, evidence } = await executeAnalysisTool({ session, openedTarget: opened.value, @@ -232,6 +263,41 @@ const runAnalysis = async ( evidenceProfile, }); if (evidence !== undefined) session.recordEvidence(evidence); + if ( + isWorkflowEvidenceTool(tool) && + tool !== "trace_native_ui_action" && + snapshotPath !== undefined && + evidence !== undefined && + "analysis_profile" in evidence && + session.allowsSnapshotReplay(tool) + ) { + const recorded = session.recordWorkflowSnapshot({ + operation: tool, + parameters: arguments_, + execution: { + result: evidence.normalized_result, + rawResult: evidence.raw_result, + provider: committedProviderSchema.parse(evidence.provider), + analysisProfile: analysisProfileSchema.parse( + evidence.analysis_profile, + ), + limitations: evidence.limitations, + locations: evidence.locations, + subject: + evidence.subject === null + ? null + : { + path: evidence.subject.local_path, + sha256: evidence.subject.digest.sha256, + format: evidence.subject.format, + ...(evidence.subject.architecture === null + ? {} + : { architecture: evidence.subject.architecture }), + }, + }, + }); + if (!recorded.ok) return cliError(recorded.error); + } if ( tool !== "trace_native_ui_action" && snapshotPath !== undefined && @@ -252,12 +318,6 @@ const runAnalysis = async ( } }; -const canReplayDirectTool = ( - session: BinarySession, - tool: Parameters[2], -): boolean => - tool !== "trace_native_ui_action" && session.allowsSnapshotReplay(tool); - const executeAnalysisTool = async (input: { readonly session: BinarySession; readonly openedTarget: BinaryTarget; @@ -353,7 +413,10 @@ const prepareSnapshot = async (options: { readonly formatHint?: ExecutableFormatHint; readonly snapshotPath: string | undefined; }): Promise< - Result<{ readonly snapshot?: AnalysisSnapshot }, AnalysisError> + Result< + { readonly snapshot?: AnalysisSnapshot; readonly target?: BinaryTarget }, + AnalysisError + > > => { const { path, snapshotPath } = options; if (snapshotPath === undefined || !(await fileExists(snapshotPath))) @@ -374,20 +437,52 @@ const prepareSnapshot = async (options: { "Analysis snapshot target does not match the requested binary", ), ); - return ok({ snapshot: loaded.value }); + return ok({ snapshot: loaded.value, target: target.value }); }; -const replayProviderFor = ( - session: BinarySession, +const allowsSnapshotReplay = ( + route: SessionProviderRoute, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, -) => - isWorkflowEvidenceTool(tool) - ? REA_WORKFLOW_PROVIDER - : session.providerIdentity(tool); +): boolean => { + if (tool === "trace_native_ui_action") return false; + const descriptor = route.capabilities?.get(tool); + return descriptor === undefined + ? ![...(route.capabilities?.values() ?? [])].some( + ({ cachePolicy }) => cachePolicy === "live", + ) + : descriptor.cachePolicy !== "live"; +}; + +const analysisProfileForRoute = ( + route: SessionProviderRoute, + tool: + | NativeToolName + | ArtifactAnalysisOperation + | ManagedToolName + | DirectAnalysisTool, +): AnalysisProfileCommitment | undefined => { + const profile = route.profile; + if (profile === null || profile === undefined) return undefined; + if (isWorkflowEvidenceTool(tool)) return workflowAnalysisProfile(profile); + const provider = providerIdentityForRoute(route, tool); + return provider.id === profile.provider.id ? profile : undefined; +}; + +/** Mirror BinarySession.providerIdentity for a route not opened yet. */ +const providerIdentityForRoute = ( + route: SessionProviderRoute, + operation: string, +): ProviderIdentity => { + const selected = + route.capabilities?.get(operation)?.provider ?? route.identity; + return route.profile?.provider.id === selected.id + ? route.profile.provider + : selected; +}; const analysisProfileForEvidence = ( session: BinarySession, diff --git a/src/application/Doctor.fixture.ts b/src/application/Doctor.fixture.ts index 2c4725dfac..9f271ed221 100644 --- a/src/application/Doctor.fixture.ts +++ b/src/application/Doctor.fixture.ts @@ -26,7 +26,8 @@ export const createDoctorHostFixture = ( Promise.resolve({ version: PRODUCT_IDENTITY.skillVersion, toolCount: CATALOG_IDENTITY.counts.mcp_tools, - catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, + catalogDigest: null, + canonical: true, }), ...overrides, }); diff --git a/src/application/Doctor.test.ts b/src/application/Doctor.test.ts index 81e4009b4e..d6845245a1 100644 --- a/src/application/Doctor.test.ts +++ b/src/application/Doctor.test.ts @@ -22,7 +22,7 @@ describe("doctor", () => { ), ).toEqual({ node: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`, - host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.", + host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, CachyOS, and the experimental Windows x64 Ghidra P0 boundary.", hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.", target: "Supply a readable local app or program path.", }); @@ -177,6 +177,7 @@ describe("doctor installation identity", () => { version: "10", toolCount: null, catalogDigest: null, + canonical: false, }), }), ); diff --git a/src/application/Doctor.ts b/src/application/Doctor.ts index a4cf57e9cc..078db0d73d 100644 --- a/src/application/Doctor.ts +++ b/src/application/Doctor.ts @@ -14,6 +14,7 @@ import { readLinuxDistribution, type LinuxDistribution, } from "./LinuxHopper.js"; +import { canonicalSkillNeedsInstall } from "./SetupSkill.js"; import { CATALOG_IDENTITY } from "../catalogIdentity.js"; import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js"; import { @@ -95,11 +96,13 @@ export interface DoctorHost { ilspyCmdVersion?(path: string): Promise; } -/** Parsed identity committed by an installed REA skill. */ +/** Observed skill metadata and comparison with the packaged instruction bundle. */ interface InstalledSkillIdentity { readonly version: string | null; readonly toolCount: number | null; readonly catalogDigest: string | null; + /** Whether all managed instruction and reference files match this package. */ + readonly canonical: boolean; } /** Structured result returned by the read-only doctor workflow. */ @@ -139,6 +142,7 @@ interface DoctorIdentity { readonly skill: { readonly installed_version: string | null; readonly installed_tool_count: number | null; + /** Legacy catalog digest observed in older bundles; current bundles omit it. */ readonly installed_catalog_digest: string | null; readonly state: "aligned" | "stale" | "missing"; readonly remediation: string | null; @@ -253,7 +257,7 @@ const skillIdentityAligned = ( ): boolean => identity?.version === PRODUCT_IDENTITY.skillVersion && identity.toolCount === CATALOG_IDENTITY.counts.mcp_tools && - identity.catalogDigest === CATALOG_IDENTITY.digests.combined_sha256; + identity.canonical; const skillIdentityCheck = ( identity: InstalledSkillIdentity | undefined, @@ -507,6 +511,7 @@ const installedSkillIdentity = async ( const content = await readInstalledSkill(home); const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1]; return { + canonical: !(await canonicalSkillNeedsInstall(home)), version: /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null, toolCount: countText === undefined ? null : Number.parseInt(countText, 10), diff --git a/src/application/DoctorDiagnostics.ts b/src/application/DoctorDiagnostics.ts index 1fe411e2d3..0baf56b501 100644 --- a/src/application/DoctorDiagnostics.ts +++ b/src/application/DoctorDiagnostics.ts @@ -115,7 +115,7 @@ const hostCheck = async (host: DoctorHost): Promise => { : `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`); return check("host", supported, detail, { remediation: - "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.", + "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, CachyOS, and the experimental Windows x64 Ghidra P0 boundary.", classification: "unsupported_host", }); }; diff --git a/src/application/EnhancedLiteralTracing.test.ts b/src/application/EnhancedLiteralTracing.test.ts deleted file mode 100644 index 42e61974ed..0000000000 --- a/src/application/EnhancedLiteralTracing.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { ok } from "../domain/result.js"; -import { traceLiteralFeature } from "./EnhancedLiteralTracing.js"; - -describe("literal feature tracing", () => { - it("returns matched procedure and string facts with references", async () => { - const result = await traceLiteralFeature( - async (name) => { - if (name === "search_strings") - return ok([{ address: "0x1000", value: "launch feature" }]); - if (name === "search_procedures") - return ok([{ address: "0x2000", value: "launchFeature" }]); - if (name === "xrefs") return ok([]); - if (name === "resolve_containing_procedure") return ok(null); - throw new Error(`Unexpected analysis call: ${name}`); - }, - { query: "launch feature", case_sensitive: false }, - ); - expect(result).toMatchObject({ - ok: true, - value: { - matches: [ - { type: "string", address: "0x1000", value: "launch feature" }, - { type: "procedure", address: "0x2000", value: "launchFeature" }, - ], - references: [], - }, - }); - }); -}); diff --git a/src/application/EvidenceBundleFiles.ts b/src/application/EvidenceBundleFiles.ts index cf59d41097..6e4c7cda0a 100644 --- a/src/application/EvidenceBundleFiles.ts +++ b/src/application/EvidenceBundleFiles.ts @@ -1,4 +1,6 @@ import { + describeEvidenceBundleFailure, + describeValidationFailure, parseEvidenceBundle, serializeEvidenceBundle, type EvidenceBundle, @@ -8,7 +10,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; @@ -20,20 +22,31 @@ export const readEvidenceBundle = async ( ): Promise> => { const loaded = await readJsonFile(path); if (!loaded.ok) return loaded; + let bundle: EvidenceBundle; try { - const bundle = parseEvidenceBundle(loaded.value); - for (const record of bundle.records) { - if (record.predicate_type === "rea.process-capture") - parseProcessCapture(record.normalized_result); - } - return ok(bundle); + bundle = parseEvidenceBundle(loaded.value); } catch (cause: unknown) { return err( new EvidenceIntegrityError("Evidence bundle validation failed", { cause, + userMessage: describeEvidenceBundleFailure(loaded.value, cause), }), ); } + for (const record of bundle.records) { + if (record.predicate_type !== "rea.process-capture") continue; + try { + parseProcessCapture(record.normalized_result); + } catch (cause: unknown) { + return err( + new EvidenceIntegrityError("Evidence bundle validation failed", { + cause, + userMessage: `Process capture record ${record.evidence_id} has an invalid normalized_result (${describeValidationFailure(cause)}). Recreate or re-export the bundle, then try again.`, + }), + ); + } + } + return ok(bundle); }; /** Atomically write deterministic evidence JSON at the caller-supplied path. */ diff --git a/src/application/JsonFiles.ts b/src/application/JsonFiles.ts index 23a3514ddc..4fea078a86 100644 --- a/src/application/JsonFiles.ts +++ b/src/application/JsonFiles.ts @@ -10,10 +10,14 @@ import { err, ok, type Result } from "../domain/result.js"; export const readJsonFile = async ( path: string, ): Promise> => { + const requestedPath = resolve(path); try { - const canonicalPath = await realpath(resolve(path)); + const canonicalPath = await realpath(requestedPath); const stats = await lstat(canonicalPath); - if (!stats.isFile()) return err(new EvidenceFileError("read", "not-file")); + if (!stats.isFile()) + return err( + new EvidenceFileError("read", "not-file", { path: requestedPath }), + ); const encoded = await readFile(canonicalPath); let decoded: unknown; try { @@ -23,11 +27,21 @@ export const readJsonFile = async ( ), ); } catch (cause: unknown) { - return err(new EvidenceFileError("read", "invalid-json", { cause })); + return err( + new EvidenceFileError("read", "invalid-json", { + cause, + path: requestedPath, + }), + ); } return ok(decoded); } catch (cause: unknown) { - return err(new EvidenceFileError("read", "io", { cause })); + return err( + new EvidenceFileError("read", missingOrIo(cause), { + cause, + path: requestedPath, + }), + ); } }; @@ -40,8 +54,8 @@ export const writeTextFile = async ( Result<{ readonly path: string; readonly bytes: number }, EvidenceFileError> > => { const bytes = Buffer.byteLength(encoded, "utf8"); + const requestedPath = resolve(path); try { - const requestedPath = resolve(path); const canonicalParent = await realpath(dirname(requestedPath)); const destination = resolve(canonicalParent, basename(requestedPath)); const existing = await lstat(destination).catch((cause: unknown) => { @@ -49,9 +63,14 @@ export const writeTextFile = async ( throw cause; }); if (existing !== undefined) { - if (!overwrite) return err(new EvidenceFileError("write", "exists")); + if (!overwrite) + return err( + new EvidenceFileError("write", "exists", { path: requestedPath }), + ); if (!existing.isFile() || existing.isSymbolicLink()) - return err(new EvidenceFileError("write", "not-file")); + return err( + new EvidenceFileError("write", "not-file", { path: requestedPath }), + ); } if (overwrite) await writeFileAtomic(destination, encoded, { @@ -63,11 +82,27 @@ export const writeTextFile = async ( return ok({ path: requestedPath, bytes }); } catch (cause: unknown) { if (!overwrite && fileErrorCode(cause) === "EEXIST") - return err(new EvidenceFileError("write", "exists", { cause })); - return err(new EvidenceFileError("write", "io", { cause })); + return err( + new EvidenceFileError("write", "exists", { + cause, + path: requestedPath, + }), + ); + return err( + new EvidenceFileError("write", missingOrIo(cause), { + cause, + path: requestedPath, + }), + ); } }; +/** A missing path or parent is a selection error, not a permission failure. */ +const missingOrIo = (cause: unknown): "missing" | "io" => { + const code = fileErrorCode(cause); + return code === "ENOENT" || code === "ENOTDIR" ? "missing" : "io"; +}; + const publishNewFile = async ( destination: string, encoded: string, diff --git a/src/application/LinuxHopper.test.ts b/src/application/LinuxHopper.test.ts index af2e05088f..be115cc71a 100644 --- a/src/application/LinuxHopper.test.ts +++ b/src/application/LinuxHopper.test.ts @@ -59,6 +59,7 @@ describe("Linux Hopper host classification", () => { ['ID=ubuntu\nVERSION_ID="24.04"\n', "deb"], ["ID=fedora\nVERSION_ID=41\n", "rpm"], ["ID=arch\n", "arch"], + ["ID=cachyos\nID_LIKE=arch\n", "arch"], ] as const)("accepts an official Hopper distribution", (document, family) => { expect(parseLinuxDistribution(document)).toMatchObject({ packageFamily: family, @@ -88,6 +89,8 @@ describe("Linux Hopper host classification", () => { 'ID=ubuntu\nVERSION_ID="22.04"\n', "ID=fedora\nVERSION_ID=40\n", 'ID=debian\nVERSION_ID="13"\nID_LIKE=debian\n', + "ID=manjaro\nID_LIKE=arch\n", + "ID=garuda\nID_LIKE=arch\n", ])("rejects unsupported vendor/version combinations", (document) => { expect(parseLinuxDistribution(document).supported).toBe(false); }); diff --git a/src/application/LinuxHopper.ts b/src/application/LinuxHopper.ts index 4840ac4d5e..652c3d5b59 100644 --- a/src/application/LinuxHopper.ts +++ b/src/application/LinuxHopper.ts @@ -150,6 +150,7 @@ export const parseLinuxDistribution = (text: string): LinuxDistribution => { const supported = packageFamily !== undefined && (id === "arch" || + id === "cachyos" || (id === "ubuntu" && versionAtLeast(versionId, 24)) || (id === "fedora" && versionAtLeast(versionId, 41))); const identity = { diff --git a/src/application/ProcessCaptureLifecycle.ts b/src/application/ProcessCaptureLifecycle.ts deleted file mode 100644 index 1ea4b47e97..0000000000 --- a/src/application/ProcessCaptureLifecycle.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { createHash, randomUUID } from "node:crypto"; -import { createReadStream } from "node:fs"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import type { IPty } from "@lydell/node-pty"; - -import type { - FilesystemCheckpoint, - InteractionEvent, - ProcessCapture, - UnverifiedProcessCapture, - ProcessSample, - ProcessSettlement, - ProcessScenario, - ProcessCaptureEventJournalEntry, - RecordProcessCaptureEvent, - TerminalFrame, -} from "../domain/processCapture.js"; -import { - digestProcessCommitment, - processComparisonContract, - processScenarioCommitment, -} from "../domain/processCapture.js"; -import { PRODUCT_IDENTITY } from "../identity.js"; -import type { SnapshotResult } from "./FilesystemSnapshot.js"; -import { snapshotRoots } from "./FilesystemSnapshot.js"; -import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js"; -import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js"; -import { observeOwnedProcessGroup } from "../process/ProcessOwnershipObservation.js"; -import { ProcessCaptureError } from "./ProcessCaptureError.js"; -import { assertNotCancelled } from "./ProcessScenarioRuntimeValidation.js"; -import { - normalizeProcessElapsedTime, - normalizeProcessSamples, - normalizeProcessText, -} from "./ProcessNormalization.js"; -import { PROCESS_PROVIDER } from "./ProcessEvidence.js"; -import { TerminalRenderer } from "./TerminalRenderer.js"; -import { scheduleProcessInterval, type ProcessTimer } from "./ProcessTimer.js"; - -interface TerminalExitOptions { - readonly terminal: IPty; - readonly scenario: ProcessScenario; - readonly started: number; - readonly lastOutput: () => number; - readonly signal: AbortSignal | undefined; - readonly timers: Set; - readonly interactions: InteractionEvent[]; - readonly dispatchedEventIndexes: ReadonlySet; - readonly recordEvent: RecordProcessCaptureEvent; -} - -interface CaptureResultOptions { - readonly frames: readonly TerminalFrame[]; - readonly exit: { - readonly exitCode: number; - readonly signal?: number; - readonly reason: "exited" | "timeout" | "idle_timeout"; - }; - readonly samples: readonly ProcessSample[]; - readonly before: SnapshotResult; - readonly after: SnapshotResult; - readonly truncated: boolean; - readonly scenario: ProcessScenario; - readonly rootPid: number; - readonly samplingPartial: boolean; - readonly renderedFrames: UnverifiedProcessCapture["rendered_frames"]; - readonly interactions: readonly InteractionEvent[]; - readonly checkpoints: readonly FilesystemCheckpoint[]; - readonly settlement: ObservedProcessSettlement; - readonly manifest: UnverifiedProcessCapture["manifest"]; - readonly eventJournal: readonly ProcessCaptureEventJournalEntry[]; -} - -/** Settlement observation before process-resource cleanup has completed. */ -export type ObservedProcessSettlement = - | Pick< - Extract, - "state" | "elapsed_ms" - > - | Pick< - Exclude, - "state" | "elapsed_ms" - >; - -/** Capture observations before owned-resource cleanup has been verified. */ -export type PendingProcessCapture = Omit< - UnverifiedProcessCapture, - "cleanup" | "settlement" -> & { - readonly settlement: ObservedProcessSettlement; -}; - -/** Wait for trailing PTY callbacks before the capture becomes immutable. */ -export const settleProcessCaptureJournal = async ( - journal: readonly ProcessCaptureEventJournalEntry[], - quietMs = 25, - maxWaitMs = 500, -): Promise => { - const deadline = Date.now() + maxWaitMs; - let observed = journal.length; - let lastChangeAt = Date.now(); - while (Date.now() < deadline) { - await new Promise((resolve) => setTimeout(resolve, 4)); - if (journal.length !== observed) { - observed = journal.length; - lastChangeAt = Date.now(); - } else if (Date.now() - lastChangeAt >= quietMs) return; - } -}; - -export const buildCaptureResult = ( - options: CaptureResultOptions, -): PendingProcessCapture => { - const hasSensitiveScriptedInput = options.scenario.events.some( - (event) => event.type === "input" && event.sensitive, - ); - const sensitiveInputUnknown = - "Sensitive scripted input values are redacted from process capture Evidence."; - const filesystemObservationUnknown = - "No filesystem observation paths were selected; filesystem effects remain unknown."; - const hasFilesystemObservations = - options.scenario.filesystem_observation_paths.length > 0; - return { - manifest: options.manifest, - normalization: options.scenario.normalization, - frames: options.frames, - rendered_frames: options.renderedFrames, - interaction_events: options.interactions, - exit: { - code: - options.exit.reason === "exited" && options.exit.exitCode >= 0 - ? options.exit.exitCode - : null, - signal: options.exit.signal ?? null, - reason: options.exit.reason, - }, - settlement: options.settlement, - process_samples: normalizeProcessSamples( - options.samples, - options.scenario, - options.rootPid, - ), - filesystem_checkpoints: options.checkpoints, - event_journal: options.eventJournal, - files_before: options.before.files, - files_after: options.after.files, - filesystem_effects: classifyFilesystemEffects( - options.before.files, - options.after.files, - ), - truncated: options.truncated, - limitations: [ - "Process trees are sampled and may omit short-lived descendants.", - ...(options.samplingPartial - ? ["Process-tree sampling ended with an incomplete observation."] - : []), - "Filesystem observations are before/after snapshots, not syscall traces.", - "Inherited host environment variables are not recorded and may affect results.", - ...(!hasFilesystemObservations ? [filesystemObservationUnknown] : []), - ...(hasSensitiveScriptedInput ? [sensitiveInputUnknown] : []), - ], - residual_unknowns: [ - { - scope: "process", - reason: - "Process trees are sampled and may omit short-lived descendants.", - }, - { - scope: "environment", - reason: "Inherited host environment variables are not recorded.", - }, - { - scope: "network", - reason: "Network activity is not observed by this capture.", - }, - ...(!hasFilesystemObservations - ? [ - { - scope: "filesystem" as const, - reason: filesystemObservationUnknown, - }, - ] - : []), - ...(hasSensitiveScriptedInput - ? [{ scope: "interaction" as const, reason: sensitiveInputUnknown }] - : []), - ], - }; -}; - -const hashFile = async (path: string): Promise => { - const hash = createHash("sha256"); - for await (const chunk of createReadStream(path)) hash.update(chunk); - return hash.digest("hex"); -}; - -export const createRunManifest = async ( - scenario: ProcessScenario, - startedAt: Date, - completedAt: Date, - host: { - readonly platform: NodeJS.Platform; - readonly architecture: NodeJS.Architecture; - } = { - platform: process.platform, - architecture: process.arch, - }, -): Promise => { - const executableSha256 = await hashFile(scenario.executable); - const scenarioCommitment = processScenarioCommitment( - scenario, - executableSha256, - ); - const comparisonContract = processComparisonContract(scenario); - return { - rea_version: PRODUCT_IDENTITY.packageVersion, - provider_version: PROCESS_PROVIDER.version, - platform: host.platform, - architecture: host.architecture, - pty_backend: "node-pty", - started_at: startedAt.toISOString(), - completed_at: completedAt.toISOString(), - scenario: scenarioCommitment, - comparison_contract: comparisonContract, - full_scenario_sha256: digestProcessCommitment(scenarioCommitment), - comparison_contract_sha256: digestProcessCommitment(comparisonContract), - executable_sha256: executableSha256, - normalization_sha256: digestProcessCommitment(scenario.normalization), - }; -}; - -export const observeSettlement = async ( - runId: string, - processGroupIds: readonly number[], - settleMs: number, - recordEvent: RecordProcessCaptureEvent = () => undefined, - platform: NodeJS.Platform = process.platform, -): Promise => { - if (platform === "win32") { - recordEvent("lifecycle", 1); - return { state: "unverifiable", elapsed_ms: 0 }; - } - const started = Date.now(); - let consecutiveEmpty = 0; - let deadlineReached = false; - while (!deadlineReached) { - const observations = await Promise.all( - [...new Set(processGroupIds)].map((processGroupId) => - observeOwnedProcessGroup({ - runId, - leaderPid: processGroupId, - processGroupId, - }), - ), - ); - if (observations.some(({ state }) => state === "unverifiable")) { - recordEvent("lifecycle", 1); - return { state: "unverifiable", elapsed_ms: Date.now() - started }; - } - if (observations.every(({ state }) => state === "empty")) { - consecutiveEmpty += 1; - if (consecutiveEmpty >= 2) { - recordEvent("lifecycle", 1); - return { state: "quiesced", elapsed_ms: Date.now() - started }; - } - } else consecutiveEmpty = 0; - deadlineReached = Date.now() - started >= settleMs; - if (deadlineReached) break; - await new Promise((resolve) => setTimeout(resolve, 50)); - } - recordEvent("lifecycle", 1); - return { state: "alive_at_deadline", elapsed_ms: Date.now() - started }; -}; - -export const awaitTerminalExit = async ({ - terminal, - scenario, - started, - lastOutput, - signal, - timers, - interactions, - dispatchedEventIndexes, - recordEvent, -}: TerminalExitOptions): Promise<{ - exitCode: number; - signal?: number; - reason: "exited" | "timeout" | "idle_timeout" | "cancelled"; -}> => - new Promise((resolveExit) => { - // The kill caused by a deadline is observed later as an ordinary PTY exit. - // Keep the initiating lifecycle reason so comparisons distinguish a target - // exit from harness-owned timeout, idle-timeout, and cancellation cleanup. - let reason: "exited" | "timeout" | "idle_timeout" | "cancelled" = "exited"; - terminal.onExit((exit) => { - recordEvent("lifecycle", 0); - for (const [eventIndex, event] of scenario.events.entries()) { - if (dispatchedEventIndexes.has(eventIndex)) continue; - const sequence = interactions.length; - interactions.push({ - sequence, - scheduled_at_ms: event.at_ms, - dispatched_at_ms: Math.max(0, Date.now() - started), - type: event.type, - data: - event.type === "input" - ? "" - : event.type === "resize" - ? `${String(event.columns)}x${String(event.rows)}` - : event.signal, - outcome: "target_exited", - }); - recordEvent("interaction_events", sequence); - } - for (const timer of timers) { - timer.cancel(); - } - timers.clear(); - resolveExit({ ...exit, reason }); - }); - const timeout = scheduleProcessInterval(() => { - if (signal?.aborted === true) { - reason = "cancelled"; - terminal.kill("SIGKILL"); - } else if (Date.now() - started >= scenario.timeout_ms) { - reason = "timeout"; - terminal.kill("SIGKILL"); - } else if (Date.now() - lastOutput() >= scenario.idle_timeout_ms) { - reason = "idle_timeout"; - terminal.kill("SIGKILL"); - } - }, 20); - timers.add(timeout); - }); - -/** Host operations used when releasing one captured process run. */ -export interface ProcessCaptureCleanupHost { - readonly platform: NodeJS.Platform; - readonly cleanupProcessGroup: typeof cleanupOwnedProcessGroup; - readonly removeTemporaryRoot: (path: string) => Promise; -} - -const processCaptureCleanupHost: ProcessCaptureCleanupHost = { - platform: process.platform, - cleanupProcessGroup: cleanupOwnedProcessGroup, - removeTemporaryRoot: (path) => rm(path, { recursive: true, force: true }), -}; - -export const releaseProcessResources = async (options: { - readonly timers: ReadonlySet; - readonly terminal: IPty | undefined; - readonly renderer: TerminalRenderer | undefined; - readonly runId: string; - readonly temporaryRoot: string; - readonly capturedProcessGroupIds: readonly number[]; - readonly host?: ProcessCaptureCleanupHost; -}): Promise => { - const host = options.host ?? processCaptureCleanupHost; - for (const timer of options.timers) timer.cancel(); - let failure: string | undefined; - try { - await options.renderer?.dispose(); - } catch (cause: unknown) { - void cause; - failure ??= "terminal renderer cleanup failed"; - } - if (options.terminal !== undefined && host.platform !== "win32") { - for (const processGroupId of new Set(options.capturedProcessGroupIds)) { - const cleaned = await host.cleanupProcessGroup({ - runId: options.runId, - leaderPid: processGroupId, - processGroupId, - }); - if (!cleaned.cleaned) failure ??= cleaned.reason; - } - } - try { - await host.removeTemporaryRoot(options.temporaryRoot); - } catch (cause: unknown) { - void cause; - failure ??= "temporary process root cleanup failed"; - } - return failure; -}; - -export const captureTerminalFrames = (options: { - readonly terminal: IPty; - readonly scenario: ProcessScenario; - readonly frames: TerminalFrame[]; - readonly started: number; - readonly temporaryRoot: string; - readonly onOutput: () => void; - readonly renderer: TerminalRenderer; - readonly recordEvent: RecordProcessCaptureEvent; -}): (() => boolean) => { - let outputBytes = 0; - let truncated = false; - options.terminal.onData((data) => { - options.onOutput(); - const bytes = Buffer.byteLength(data); - if (outputBytes + bytes > options.scenario.limits.output_bytes) { - truncated = true; - return; - } - outputBytes += bytes; - const atMs = normalizeProcessElapsedTime( - Date.now() - options.started, - options.scenario.normalization.time_bucket_ms, - ); - const normalized = normalizeProcessText( - data, - options.scenario, - options.temporaryRoot, - options.terminal.pid, - ); - const sequence = options.frames.length; - options.frames.push({ - sequence, - at_ms: atMs, - data: normalized, - }); - options.renderer.write(data, atMs); - options.recordEvent("frames", sequence); - }); - return () => truncated; -}; - -export const resolveProcessResult = ( - capture: ProcessCapture | undefined, - executionFailure: unknown, - cleanupFailure: string | undefined, -): ProcessCapture => { - if (executionFailure instanceof ProcessCaptureError) throw executionFailure; - if (executionFailure !== undefined) - throw new ProcessCaptureError("process capture failed", { - cause: executionFailure, - }); - if (cleanupFailure !== undefined) - throw new ProcessCaptureError(cleanupFailure, { - reason: "cleanup_incomplete", - }); - if (capture === undefined) - throw new ProcessCaptureError("process capture produced no result"); - return capture; -}; - -export const prepareProcessCapture = async ( - scenario: ProcessScenario, - signal: AbortSignal | undefined, - host: ProcessPreparationHost = systemProcessPreparationHost, -): Promise<{ - readonly temporaryRoot: string; - readonly runId: string; - readonly before: SnapshotResult; -}> => { - assertNotCancelled(signal); - const before = await snapshotRoots(scenario, signal); - const temporaryRoot = await host.createTemporaryRoot(); - try { - const runId = randomUUID(); - return { temporaryRoot, runId, before }; - } catch (cause: unknown) { - await host.cleanup(temporaryRoot); - throw cause; - } -}; - -/** Filesystem seam for allocating and cleaning a capture's temporary root. */ -export interface ProcessPreparationHost { - createTemporaryRoot(): Promise; - cleanup(path: string): Promise; -} - -const systemProcessPreparationHost: ProcessPreparationHost = { - createTemporaryRoot: () => mkdtemp(join(tmpdir(), "rea-process-")), - cleanup: (path) => rm(path, { recursive: true, force: true }), -}; diff --git a/src/application/ProcessHarness.test.ts b/src/application/ProcessHarness.test.ts deleted file mode 100644 index 8c2d38bfc8..0000000000 --- a/src/application/ProcessHarness.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { expect, it } from "vitest"; -import { processScenarioSchema } from "../domain/processScenario.js"; -import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; -import { captureProcessScenario } from "./ProcessHarness.js"; -import { settleProcessCaptureJournal } from "./ProcessCaptureLifecycle.js"; -import { processCaptureSchema } from "../domain/processCapture.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; - -it("rejects legacy replay output instead of silently discarding it", () => { - expect( - processCaptureSchema.safeParse({ - ...EMPTY_PROCESS_CAPTURE_EXAMPLE, - protocol_events: [], - }).success, - ).toBe(false); -}); - -it("waits for terminal observations delivered after the exit callback", async () => { - const journal: Array<{ - capture_order: number; - collection: "frames"; - index: number; - }> = [{ capture_order: 0, collection: "frames", index: 0 }]; - const lateFrame = new Promise((resolve) => { - setTimeout(() => { - journal.push({ capture_order: 1, collection: "frames", index: 1 }); - resolve(); - }, 12); - }); - - let settled = false; - const wait = settleProcessCaptureJournal(journal, 20, 200).then(() => { - settled = true; - }); - await lateFrame; - expect(settled).toBe(false); - await wait; - - expect(journal).toHaveLength(2); - expect(journal[1]).toMatchObject({ collection: "frames", index: 1 }); -}); - -it("fails closed on Windows before resolving or launching scenario paths", async () => { - const scenario = processScenarioSchema.parse({ - executable: "Z:/missing/should-never-be-resolved.exe", - working_directory: "Z:/missing/working-directory", - }); - const result = await captureProcessScenario(scenario, undefined, "win32"); - expect(result.ok).toBe(false); - if (result.ok) throw new Error("expected Windows ownership refusal"); - if (!(result.error instanceof AnalysisCapabilityUnavailableError)) - throw new Error("expected a capability-unavailable outcome"); - expect(result.error).toMatchObject({ - operation: "capture_process_scenario", - reason: expect.stringContaining("Windows PTY process capture"), - }); - expect(projectAnalysisError(result.error)).toMatchObject({ - code: "capability_unavailable", - category: "unsupported_provider", - message: expect.stringContaining("does not yet verify descendant cleanup"), - details: { - operation: "capture_process_scenario", - reason: result.error.reason, - }, - }); -}); diff --git a/src/application/ProcessNormalization.test.ts b/src/application/ProcessNormalization.test.ts deleted file mode 100644 index 9baf1e4b64..0000000000 --- a/src/application/ProcessNormalization.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { expect, it } from "vitest"; - -import { normalizeProcessSamples } from "./ProcessNormalization.js"; -import { - parseProcessScenario, - type ProcessSample, -} from "../domain/processCapture.js"; - -const rootPid = 41_010; -const samples = Object.freeze( - [ - { - at_ms: 17, - pid: rootPid, - parent_pid: 40_000, - process_group_id: rootPid, - session_id: rootPid, - command: " root 41010 ", - }, - { - at_ms: 28, - pid: 42_020, - parent_pid: rootPid, - process_group_id: 43_030, - session_id: 44_040, - command: "child 42020 parent 41010 group 43030 session 44040", - }, - { - at_ms: 39, - pid: 45_050, - parent_pid: 42_020, - process_group_id: 43_030, - session_id: 44_040, - command: "peer 45050 unrelated 145050", - }, - { - at_ms: 41, - pid: 46_060, - parent_pid: 0, - process_group_id: null, - session_id: 0, - command: "unknown 46060", - }, - { - at_ms: 52, - pid: 47_070, - parent_pid: 0, - process_group_id: 0, - session_id: null, - command: "unavailable 47070", - }, - ].map((sample) => - Object.freeze({ ...sample, runtime_metadata: "must not be serialized" }), - ), -); - -it.each([false, true, undefined])( - "projects only declared sample fields without mutating inputs when pids is %s", - (pids) => { - const scenario = parseProcessScenario({ - executable: "/usr/bin/node", - ...(pids === undefined ? {} : { normalization: { pids } }), - }); - const before = JSON.stringify(samples); - const normalized = normalizeProcessSamples(samples, scenario, rootPid); - const identities = - pids === false - ? [ - [rootPid, 40_000, rootPid, rootPid], - [42_020, rootPid, 43_030, 44_040], - [45_050, 42_020, 43_030, 44_040], - [46_060, 0, null, 0], - [47_070, 0, 0, null], - ] - : [ - [1, 2, 1, 1], - [3, 1, 4, 5], - [6, 3, 4, 5], - [7, 0, null, 0], - [8, 0, 0, null], - ]; - const commands = - pids === false - ? samples.map(({ command }) => command.trim()) - : [ - "root ", - "child parent group session ", - "peer unrelated 145050", - "unknown ", - "unavailable ", - ]; - - expect(scenario.normalization.pids).toBe(pids ?? true); - expect(normalized).toEqual( - identities.map( - ([pid, parent_pid, process_group_id, session_id], index) => ({ - at_ms: (index + 1) * 10, - pid, - parent_pid, - process_group_id, - session_id, - command: commands[index], - }), - ), - ); - expect(normalized).not.toBe(samples); - for (const [index, sample] of normalized.entries()) { - expect(sample).not.toBe(samples[index]); - expect(Object.keys(sample).sort()).toEqual( - [ - "at_ms", - "pid", - "parent_pid", - "process_group_id", - "session_id", - "command", - ].sort(), - ); - } - expect(JSON.stringify(samples)).toBe(before); - }, -); - -for (const pids of [false, true]) { - for (const paths of [false, true]) { - it.each([false, true])( - `keeps time, patterns, paths=${String(paths)}, and ports=%s independent of pids=${String(pids)}`, - (ports) => { - const sample: ProcessSample = Object.freeze({ - at_ms: 29, - pid: 42_020, - parent_pid: rootPid, - process_group_id: 43_030, - session_id: 44_040, - command: - " /workspace/program child 42020 parent 41010 group 43030 session 44040 listen:8080 marker ", - }); - const scenario = parseProcessScenario({ - executable: "/usr/bin/node", - working_directory: "/workspace", - normalization: { - pids, - paths, - ports, - time_bucket_ms: 25, - patterns: [{ pattern: "marker", replacement: "selected" }], - }, - }); - const normalized = normalizeProcessSamples([sample], scenario, rootPid); - const directory = paths ? "" : "/workspace"; - const identifiers = pids - ? "child parent group session " - : "child 42020 parent 41010 group 43030 session 44040"; - expect(normalized).toEqual([ - { - at_ms: 25, - pid: pids ? 2 : 42_020, - parent_pid: pids ? 1 : rootPid, - process_group_id: pids ? 3 : 43_030, - session_id: pids ? 4 : 44_040, - command: `${directory}/program ${identifiers} listen:${ports ? "" : "8080"} selected`, - }, - ]); - }, - ); - } -} diff --git a/src/application/ReconstructionObligationCandidates.test.ts b/src/application/ReconstructionObligationCandidates.test.ts index d3ddc3224d..0644cf4d7d 100644 --- a/src/application/ReconstructionObligationCandidates.test.ts +++ b/src/application/ReconstructionObligationCandidates.test.ts @@ -2,6 +2,14 @@ import { describe, expect, it } from "vitest"; import { createEvidence } from "../domain/evidence.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; +import { emptyUnverifiedProcessCapture } from "../domain/process/processCapture.fixture.js"; +import { parseProcessCapture } from "../domain/process/processCapture.js"; +import { + digestProcessCommitment, + parseProcessScenario, + processComparisonContract, + processScenarioCommitment, +} from "../domain/process/processScenario.js"; import { APPLICATION_GRAPH_DIGESTS, artifactEvidence, @@ -14,6 +22,7 @@ import { } from "../domain/javascript/javascriptApplicationGraph.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js"; +import { createProcessCaptureEvidence } from "./process/ProcessEvidence.js"; import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js"; const applicationWithUnknowns = (keys: readonly string[]) => { @@ -105,30 +114,13 @@ const unknownLimitation = (graphId: string, nodeId: string): string => `Application graph ${graphId} contains unresolved node ${nodeId}; reconstruction obligations remain unknown.`; describe("reconstruction obligation candidates", () => { - it("preserves admitted unknown application nodes as limitations", () => { - const { graph, unknownNodes, generated } = applicationWithUnknowns([ - "unclassified-boundary", - ]); - const unknownNode = unknownNodes[0]; - if (unknownNode === undefined) throw new Error("Expected one unknown node"); - - expect(generated.candidates.length).toBeGreaterThan(0); - expect( - generated.candidates.some( - ({ target }) => target.application_node_id === unknownNode.node_id, - ), - ).toBe(false); - expect(generated.limitations).toEqual([ - unknownLimitation(graph.graph_id, unknownNode.node_id), - ]); - }); - - it("emits one deterministic limitation per unknown node", () => { + it("preserves unresolved application nodes as deterministic limitations", () => { const { graph, unknownNodes, generated } = applicationWithUnknowns([ "unclassified-first", "unclassified-second", ]); + expect(generated.candidates.length).toBeGreaterThan(0); expect([...generated.limitations].sort()).toEqual( unknownNodes .map((node) => unknownLimitation(graph.graph_id, node.node_id)) @@ -141,4 +133,41 @@ describe("reconstruction obligation candidates", () => { ), ).toBe(false); }); + + it("does not bind an unknown process image to a packaged artifact obligation", () => { + const scenario = parseProcessScenario({ + executable: "/path/whose-launch-identity-is-unknown", + }); + const base = emptyUnverifiedProcessCapture(); + const scenarioProjection = processScenarioCommitment(scenario); + const comparisonContract = processComparisonContract(scenario); + const capture = parseProcessCapture({ + ...base, + normalization: scenario.normalization, + manifest: { + ...base.manifest, + scenario: scenarioProjection, + comparison_contract: comparisonContract, + full_scenario_sha256: digestProcessCommitment(scenarioProjection), + comparison_contract_sha256: digestProcessCommitment(comparisonContract), + selected_executable_sha256: null, + executable_sha256: null, + executable_identity: { + state: "unknown", + reason: "Selected executable could not be inspected.", + }, + normalization_sha256: digestProcessCommitment(scenario.normalization), + }, + }); + const evidence = createProcessCaptureEvidence(scenario, capture); + const generated = deriveReconstructionObligationCandidates( + createEvidenceBundle([evidence]), + [], + ); + + expect(generated.candidates).toEqual([]); + expect(generated.limitations).toContain( + `Process capture ${evidence.evidence_id} has no validated executable artifact identity; packaged-process lifecycle obligation was not bound to an artifact.`, + ); + }); }); diff --git a/src/application/ReconstructionObligationCandidates.ts b/src/application/ReconstructionObligationCandidates.ts index e977648677..0140376927 100644 --- a/src/application/ReconstructionObligationCandidates.ts +++ b/src/application/ReconstructionObligationCandidates.ts @@ -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, @@ -176,6 +176,12 @@ const deriveProcessCandidate = ( ] : []), ]; + if (capture.manifest.executable_sha256 === null) { + limitations.add( + `Process capture ${evidence.evidence_id} has no validated executable artifact identity; packaged-process lifecycle obligation was not bound to an artifact.`, + ); + return; + } addCandidate(candidates, { obligation_id: obligationId( `process:${capture.manifest.executable_sha256}:${capture.manifest.full_scenario_sha256}`, diff --git a/src/application/ReconstructionObligationLedger.authority.test.ts b/src/application/ReconstructionObligationLedger.authority.test.ts index b4a3f84ced..401088add3 100644 --- a/src/application/ReconstructionObligationLedger.authority.test.ts +++ b/src/application/ReconstructionObligationLedger.authority.test.ts @@ -1,112 +1,16 @@ import { describe, expect, it } from "vitest"; -import { PROCESS_PROVIDER } from "./ProcessEvidence.js"; import { - buildReconstructionObligationLedgerEvidenceValidated, - resolveReconstructionObligationLedgerRequest, -} from "./ReconstructionObligationLedgerService.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/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 { - reconstructionObligationLedgerSchema, - type ReconstructionObligationLedgerInput, - type ReconstructionObligationLedger, - type ReviewedReconstructionObligation, -} from "../domain/reconstructionObligationLedgerSchemas.js"; - -const proofEvidence = (id: string) => - createEvidence( - undefined, - { id: `fixture-${id}`, name: "Fixture verifier", version: "1" }, - { - predicateType: "rea.fixture-verification", - operation: "run_fixture_verifier", - parameters: { id }, - result: { passed: true }, - confidence: "observed", - authority: "controlled-replay", - }, - ); - -const processEvidence = () => - createEvidence(undefined, PROCESS_PROVIDER, { - predicateType: "rea.process-capture", - operation: "capture_process_scenario", - parameters: {}, - result: jsonValueSchema.parse( - processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE), - ), - confidence: "observed", - authority: "controlled-replay", - }); - -const boundProofEvidence = ( - id: string, - result: { - readonly obligation_ids: readonly string[]; - readonly fixture_ids: readonly string[]; - readonly case_kinds: readonly string[]; - readonly verifier_ids: readonly string[]; - readonly claim_ids: readonly string[]; - }, - authority: "shipped-artifact" | "controlled-replay" = "controlled-replay", -) => - createEvidence( - undefined, - { id: `proof-${id}`, name: "Reconstruction proof", version: "1" }, - { - predicateType: "rea.reconstruction-proof", - operation: "verify_reconstruction_obligations", - parameters: {}, - result: { - passed: true, - obligation_ids: [...result.obligation_ids], - fixture_ids: [...result.fixture_ids], - case_kinds: [...result.case_kinds], - verifier_ids: [...result.verifier_ids], - claim_ids: [...result.claim_ids], - }, - confidence: "observed", - authority, - }, - ); - -const request = ( - records: readonly Evidence[], - overrides: Partial = {}, -): ReconstructionObligationLedgerInput => ({ - evidence_bundle: createEvidenceBundle(records), - reviewed_obligations: [], - manifest: { bindings: [], contradictions: [] }, - ...overrides, -}); - -const build = ( - input: ReconstructionObligationLedgerInput, -): ReconstructionObligationLedger => { - const parsed = resolveReconstructionObligationLedgerRequest(input); - if (!parsed.ok) throw parsed.error; - const result = buildReconstructionObligationLedgerEvidenceValidated( - parsed.value, - ); - if (!result.ok) throw result.error; - return reconstructionObligationLedgerSchema.parse( - result.value.normalized_result, - ); -}; - -const originalCases = ( - obligation: ReconstructionObligationLedger["obligations"][number], - evidenceId: string, -) => - obligation.required_case_kinds.map((caseKind) => ({ - kind: caseKind, - evidence_id: evidenceId, - location: `/normalized_result/cases/${caseKind}`, - })); + boundProofEvidence, + build, + originalCases, + processEvidence, + proofEvidence, + request, + reviewedObligation, +} from "./ReconstructionObligationLedger.fixture.js"; +import { createEvidence } from "../domain/evidence.js"; +import type { ReviewedReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js"; describe("reconstruction obligation ledger", () => { it("does not authenticate packaged-process proof with generic replay evidence", () => { @@ -315,32 +219,3 @@ describe("reconstruction obligation ledger fail-closed behavior", () => { ]); }); }); - -const reviewedObligation = ( - obligationId: string, - evidenceId: string, -): ReviewedReconstructionObligation => ({ - obligation_id: obligationId, - obligation_version: 1, - title: `Reviewed obligation ${obligationId}`, - application_layer: "other", - family: "reviewed", - target: { - artifact_sha256: null, - application_node_id: null, - semantic_node_id: null, - location: `/reviewed/${obligationId}`, - }, - required: true, - required_case_kinds: ["positive"], - required_original_authority: "static", - required_fixture_authority: "unit", - required_verifier_authority: "unit", - requires_parser_type: false, - dependency_obligation_ids: [], - residual_unknown_ids: [], - unavailable_authority: [], - required_next_evidence: ["Bind an owner and verifier."], - disposition: "active", - review_evidence_ids: [evidenceId], -}); diff --git a/src/application/ReconstructionObligationLedger.closure.test.ts b/src/application/ReconstructionObligationLedger.closure.test.ts index f2bcd43d30..e11bffaddb 100644 --- a/src/application/ReconstructionObligationLedger.closure.test.ts +++ b/src/application/ReconstructionObligationLedger.closure.test.ts @@ -1,141 +1,14 @@ import { describe, expect, it } from "vitest"; -import { PROCESS_PROVIDER } from "./ProcessEvidence.js"; import { - buildReconstructionObligationLedgerEvidenceValidated, - resolveReconstructionObligationLedgerRequest, -} from "./ReconstructionObligationLedgerService.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/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 { - reconstructionObligationLedgerSchema, - type ReconstructionObligationLedgerInput, - type ReconstructionObligationLedger, - type ReviewedReconstructionObligation, -} from "../domain/reconstructionObligationLedgerSchemas.js"; - -const proofEvidence = (id: string) => - createEvidence( - undefined, - { id: `fixture-${id}`, name: "Fixture verifier", version: "1" }, - { - predicateType: "rea.fixture-verification", - operation: "run_fixture_verifier", - parameters: { id }, - result: { passed: true }, - confidence: "observed", - authority: "controlled-replay", - }, - ); - -const processEvidence = () => - createEvidence(undefined, PROCESS_PROVIDER, { - predicateType: "rea.process-capture", - operation: "capture_process_scenario", - parameters: {}, - result: jsonValueSchema.parse( - processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE), - ), - confidence: "observed", - authority: "controlled-replay", - }); - -const boundProofEvidence = ( - id: string, - result: { - readonly obligation_ids: readonly string[]; - readonly fixture_ids: readonly string[]; - readonly case_kinds: readonly string[]; - readonly verifier_ids: readonly string[]; - readonly claim_ids: readonly string[]; - }, - authority: "shipped-artifact" | "controlled-replay" = "controlled-replay", -) => - createEvidence( - undefined, - { id: `proof-${id}`, name: "Reconstruction proof", version: "1" }, - { - predicateType: "rea.reconstruction-proof", - operation: "verify_reconstruction_obligations", - parameters: {}, - result: { - passed: true, - obligation_ids: [...result.obligation_ids], - fixture_ids: [...result.fixture_ids], - case_kinds: [...result.case_kinds], - verifier_ids: [...result.verifier_ids], - claim_ids: [...result.claim_ids], - }, - confidence: "observed", - authority, - }, - ); - -const request = ( - records: readonly Evidence[], - overrides: Partial = {}, -): ReconstructionObligationLedgerInput => ({ - evidence_bundle: createEvidenceBundle(records), - reviewed_obligations: [], - manifest: { bindings: [], contradictions: [] }, - ...overrides, -}); - -const build = ( - input: ReconstructionObligationLedgerInput, -): ReconstructionObligationLedger => { - const parsed = resolveReconstructionObligationLedgerRequest(input); - if (!parsed.ok) throw parsed.error; - const result = buildReconstructionObligationLedgerEvidenceValidated( - parsed.value, - ); - if (!result.ok) throw result.error; - return reconstructionObligationLedgerSchema.parse( - result.value.normalized_result, - ); -}; - -const originalCases = ( - obligation: ReconstructionObligationLedger["obligations"][number], - evidenceId: string, -) => - obligation.required_case_kinds.map((caseKind) => ({ - kind: caseKind, - evidence_id: evidenceId, - location: `/normalized_result/cases/${caseKind}`, - })); - -const reviewedObligation = ( - obligationId: string, - evidenceId: string, -): ReviewedReconstructionObligation => ({ - obligation_id: obligationId, - obligation_version: 1, - title: `Reviewed obligation ${obligationId}`, - application_layer: "other", - family: "reviewed", - target: { - artifact_sha256: null, - application_node_id: null, - semantic_node_id: null, - location: `/reviewed/${obligationId}`, - }, - required: true, - required_case_kinds: ["positive"], - required_original_authority: "static", - required_fixture_authority: "unit", - required_verifier_authority: "unit", - requires_parser_type: false, - dependency_obligation_ids: [], - residual_unknown_ids: [], - unavailable_authority: [], - required_next_evidence: ["Bind an owner and verifier."], - disposition: "active", - review_evidence_ids: [evidenceId], -}); + boundProofEvidence, + build, + originalCases, + processEvidence, + proofEvidence, + request, + reviewedObligation, +} from "./ReconstructionObligationLedger.fixture.js"; describe("reconstruction obligation ledger", () => { it("emits the complete deterministic ledger and closure identity", () => { diff --git a/src/application/ReconstructionObligationLedger.fixture.ts b/src/application/ReconstructionObligationLedger.fixture.ts new file mode 100644 index 0000000000..1892a4442e --- /dev/null +++ b/src/application/ReconstructionObligationLedger.fixture.ts @@ -0,0 +1,143 @@ +import { PROCESS_PROVIDER } from "./process/ProcessEvidence.js"; +import { + buildReconstructionObligationLedgerEvidenceValidated, + resolveReconstructionObligationLedgerRequest, +} from "./ReconstructionObligationLedgerService.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/process/processCapture.js"; +import { + reconstructionObligationLedgerSchema, + type ReconstructionObligationLedgerInput, + type ReconstructionObligationLedger, + type ReviewedReconstructionObligation, +} from "../domain/reconstructionObligationLedgerSchemas.js"; + +/** Build controlled fixture Evidence used by reconstruction-ledger scenarios. */ +export const proofEvidence = (id: string) => + createEvidence( + undefined, + { id: `fixture-${id}`, name: "Fixture verifier", version: "1" }, + { + predicateType: "rea.fixture-verification", + operation: "run_fixture_verifier", + parameters: { id }, + result: { passed: true }, + confidence: "observed", + authority: "controlled-replay", + }, + ); + +/** Use the canonical empty process capture as a controlled Evidence example. */ +export const processEvidence = () => + createEvidence(undefined, PROCESS_PROVIDER, { + predicateType: "rea.process-capture", + operation: "capture_process_scenario", + parameters: {}, + result: jsonValueSchema.parse( + processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE), + ), + confidence: "observed", + authority: "controlled-replay", + }); + +/** Build verifier Evidence bound to enumerated fixture and obligation identities. */ +export const boundProofEvidence = ( + id: string, + result: { + readonly obligation_ids: readonly string[]; + readonly fixture_ids: readonly string[]; + readonly case_kinds: readonly string[]; + readonly verifier_ids: readonly string[]; + readonly claim_ids: readonly string[]; + }, + authority: "shipped-artifact" | "controlled-replay" = "controlled-replay", +) => + createEvidence( + undefined, + { id: `proof-${id}`, name: "Reconstruction proof", version: "1" }, + { + predicateType: "rea.reconstruction-proof", + operation: "verify_reconstruction_obligations", + parameters: {}, + result: { + passed: true, + obligation_ids: [...result.obligation_ids], + fixture_ids: [...result.fixture_ids], + case_kinds: [...result.case_kinds], + verifier_ids: [...result.verifier_ids], + claim_ids: [...result.claim_ids], + }, + confidence: "observed", + authority, + }, + ); + +/** Build a ledger request with empty reviewed input and manifest defaults. */ +export const request = ( + records: readonly Evidence[], + overrides: Partial = {}, +): ReconstructionObligationLedgerInput => ({ + evidence_bundle: createEvidenceBundle(records), + reviewed_obligations: [], + manifest: { bindings: [], contradictions: [] }, + ...overrides, +}); + +/** Run request resolution, Evidence validation, and output parsing for a fixture. */ +export const build = ( + input: ReconstructionObligationLedgerInput, +): ReconstructionObligationLedger => { + const parsed = resolveReconstructionObligationLedgerRequest(input); + if (!parsed.ok) throw parsed.error; + const result = buildReconstructionObligationLedgerEvidenceValidated( + parsed.value, + ); + if (!result.ok) throw result.error; + return reconstructionObligationLedgerSchema.parse( + result.value.normalized_result, + ); +}; + +/** Create provenance for each required case from one source Evidence record. */ +export const originalCases = ( + obligation: ReconstructionObligationLedger["obligations"][number], + evidenceId: string, +) => + obligation.required_case_kinds.map((caseKind) => ({ + kind: caseKind, + evidence_id: evidenceId, + location: `/normalized_result/cases/${caseKind}`, + })); + +/** Create a minimal reviewed obligation for ledger closure scenarios. */ +export const reviewedObligation = ( + obligationId: string, + evidenceId: string, +): ReviewedReconstructionObligation => ({ + obligation_id: obligationId, + obligation_version: 1, + title: `Reviewed obligation ${obligationId}`, + application_layer: "other", + family: "reviewed", + target: { + artifact_sha256: null, + application_node_id: null, + semantic_node_id: null, + location: `/reviewed/${obligationId}`, + }, + required: true, + required_case_kinds: ["positive"], + required_original_authority: "static", + required_fixture_authority: "unit", + required_verifier_authority: "unit", + requires_parser_type: false, + dependency_obligation_ids: [], + residual_unknown_ids: [], + unavailable_authority: [], + required_next_evidence: ["Bind an owner and verifier."], + disposition: "active", + review_evidence_ids: [evidenceId], +}); diff --git a/src/application/Setup.planning.test.ts b/src/application/Setup.planning.test.ts index 3654c2dcf2..dbcd96f6f3 100644 --- a/src/application/Setup.planning.test.ts +++ b/src/application/Setup.planning.test.ts @@ -165,15 +165,6 @@ describe("setup workflow lifecycle and provider planning", () => { remediation: null, }, ]; - host.productRegistrations = [ - { - client: "codex", - config_path: "/codex.toml", - command: ["rea", "mcp"], - state: "aligned", - remediation: null, - }, - ]; const progress: SetupProgressEvent[] = []; await runSetup( { ...options(true), onProgress: (event) => progress.push(event) }, diff --git a/src/application/Setup.recovery.test.ts b/src/application/Setup.recovery.test.ts index 686a3d9031..57765ec236 100644 --- a/src/application/Setup.recovery.test.ts +++ b/src/application/Setup.recovery.test.ts @@ -194,7 +194,7 @@ describe("setup scoped readiness", () => { expect(result.status).toBe("needs_human"); expect(host.hopperInstalls).toBe(0); expect(result.remediation).toBe( - "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead.", + "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS; configure an existing supported provider instead.", ); }); diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts index a9a231d99a..d59b06f179 100644 --- a/src/application/SetupClientConfiguration.ts +++ b/src/application/SetupClientConfiguration.ts @@ -15,6 +15,7 @@ import { dirname } from "node:path"; import writeFileAtomic from "write-file-atomic"; import { PRODUCT_IDENTITY } from "../identity.js"; +import { npxRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js"; import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js"; import type { @@ -24,12 +25,7 @@ import type { } from "./SetupTypes.js"; import type { SetupClient } from "./SupportedClients.js"; -const defaultCommand = (): readonly string[] => [ - "npx", - "-y", - PRODUCT_IDENTITY.registrationPackageSpecifier, - "mcp", -]; +const defaultCommand = (): readonly string[] => npxRegistrationCommand(); /** Back up, atomically update, and semantically read back one JSON MCP configuration. */ export const configureJsonClient = ( diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts index 6bda5b494c..b4a8a49446 100644 --- a/src/application/SetupHost.ts +++ b/src/application/SetupHost.ts @@ -3,6 +3,7 @@ import { homedir } from "node:os"; import { resolve } from "node:path"; import { PRODUCT_IDENTITY } from "../identity.js"; +import { npxRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { SUPPORTED_NODE_VERSION_PROSE, supportsNodeVersion, @@ -35,7 +36,7 @@ export const setupRegistrationCommand = ( useNpmRunner: boolean = process.env.npm_command === "exec", ): readonly string[] => useNpmRunner - ? PRODUCT_IDENTITY.mcpCommand.split(" ") + ? npxRegistrationCommand(platform) : platform === "win32" ? [ process.execPath, @@ -82,7 +83,7 @@ export const hostRemediation = async ( : undefined; } if ((await host.linuxDistribution())?.supported === true) return undefined; - return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead."; + return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS; configure an existing supported provider instead."; }; /** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */ diff --git a/src/application/SetupInstallFailure.test.ts b/src/application/SetupInstallFailure.test.ts deleted file mode 100644 index 088fc4b917..0000000000 --- a/src/application/SetupInstallFailure.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - setupInstallFailure, - type HopperInstallFailureReason, -} from "./SetupInstallFailure.js"; - -describe("setup installer failure projection", () => { - it.each([ - ["unsupported_host", "unsupported_host"], - ["download", "download_failed"], - ["release_metadata", "download_failed"], - ["integrity", "integrity_mismatch"], - [ - "authorization_or_package_manager", - "authorization_or_package_manager_failed", - ], - ["launcher_missing", "launcher_missing"], - ["runtime_dependencies", "runtime_dependency_unavailable"], - ["unsupported_hopper_build", "unsupported_hopper_build"], - ["cancelled", "setup_cancelled"], - ["destination_exists", "destination_exists"], - ["mount", "mount_failed"], - ["bundle", "bundle_invalid"], - ["copy", "copy_failed"], - ] as const)("maps %s to %s", (reason, code) => { - const result = setupInstallFailure(reason as HopperInstallFailureReason); - expect(result).toMatchObject({ status: "failed", code }); - expect(result).toMatchObject({ remediation: expect.stringMatching(/\S/u) }); - }); -}); diff --git a/src/application/SetupRegistrationEnvironment.test.ts b/src/application/SetupRegistrationEnvironment.test.ts deleted file mode 100644 index 295794c762..0000000000 --- a/src/application/SetupRegistrationEnvironment.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import type { DoctorProviderInspection } from "./Doctor.js"; -import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js"; - -const inspection = ( - overrides: Partial = {}, -): DoctorProviderInspection => ({ - id: "fixture", - configured: true, - available: false, - providerVersion: "1", - registrationEnvironment: { - REA_FIXTURE_PROVIDER_ROOT: "C:\\Tools\\Provider", - }, - checks: [], - ...overrides, -}); - -describe("provider registration settings", () => { - it("retains valid published paths when runtime capabilities are unavailable", () => { - expect(providerRegistrationEnvironment([inspection()])).toEqual({ - REA_FIXTURE_PROVIDER_ROOT: "C:\\Tools\\Provider", - }); - }); - - it("does not invent settings when the adapter cannot publish an installation", () => { - expect( - providerRegistrationEnvironment([ - inspection({ configured: false, registrationEnvironment: {} }), - ]), - ).toEqual({}); - }); - - it("keeps available-provider settings unchanged", () => { - expect( - providerRegistrationEnvironment([inspection({ available: true })]), - ).toEqual({ REA_FIXTURE_PROVIDER_ROOT: "C:\\Tools\\Provider" }); - }); -}); diff --git a/src/application/StoreFileLock.ts b/src/application/StoreFileLock.ts deleted file mode 100644 index 4f79cc060c..0000000000 --- a/src/application/StoreFileLock.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { - lstat, - mkdtemp, - open, - readdir, - rename, - rm, - rmdir, - unlink, -} from "node:fs/promises"; -import { dirname, join } from "node:path"; - -import { readBoundedFileBytes } from "../process/BoundedFileBytes.js"; - -/** One acquired store transaction lock with cleanup bound to its unique owner. */ -export interface StoreFileLock { - readonly release: () => Promise; -} - -class StoreFileLockOccupied extends Error { - readonly code = "EEXIST"; - - constructor(cause?: unknown) { - super("Store transaction lock is occupied", { cause }); - } -} - -/** Atomically publish a populated directory so another owner cannot replace a live lock. */ -export const createStoreFileLock = async ( - path: string, -): Promise => { - if (await exists(path)) throw new StoreFileLockOccupied(); - const staged = await mkdtemp(join(dirname(path), ".rea-store-lock-")); - const owner = `owner-${randomUUID()}`; - try { - const handle = await open(join(staged, owner), "wx", 0o600); - try { - await handle.writeFile(`${String(process.pid)}\n`, "utf8"); - await handle.sync(); - } finally { - await handle.close(); - } - try { - // Rename cannot replace a nonempty directory: the owner entry is present before publication. - await rename(staged, path); - } catch (cause: unknown) { - if (await exists(path)) throw new StoreFileLockOccupied(cause); - throw cause; - } - return { - release: async () => { - await removeOwner(path, owner).catch((cause: unknown) => { - // best-effort cleanup: lock release must not reject unhandled. - void cause; - }); - }, - }; - } finally { - await rm(staged, { recursive: true, force: true }); - } -}; - -/** Recover a dead owner's directory lock or a legacy PID file; leave live and unknown owners intact. */ -export const removeStaleStoreFileLock = async ( - path: string, -): Promise => { - try { - const stats = await lstat(path); - if (stats.isFile()) { - if (!(await ownerIsDead(path))) return false; - await unlink(path); - return true; - } - if (!stats.isDirectory()) return false; - const entries = await readdir(path); - if (entries.length === 0) { - await rmdir(path); - return true; - } - const owner = entries[0]; - if ( - entries.length !== 1 || - owner === undefined || - !/^owner-[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(owner) || - !(await ownerIsDead(join(path, owner))) - ) - return false; - await removeOwner(path, owner); - return true; - } catch (cause: unknown) { - // best-effort cleanup: stale lock probing; failure means the lock stands. - void cause; - return false; - } -}; - -const removeOwner = async (path: string, owner: string): Promise => { - // A successor has a different entry, so stale cleanup cannot make its directory empty. - await unlink(join(path, owner)); - await rmdir(path); -}; - -const ownerIsDead = async (path: string): Promise => { - const stats = await lstat(path); - if (!stats.isFile() || stats.size > 32) return false; - const handle = await open(path, "r"); - let bytes: Buffer | undefined; - try { - bytes = await readBoundedFileBytes(handle, 32); - } finally { - await handle.close(); - } - const encoded = bytes?.toString("utf8").trim(); - return ( - encoded !== undefined && - /^[1-9][0-9]{0,9}$/u.test(encoded) && - !processIsAlive(Number(encoded)) - ); -}; - -const exists = async (path: string): Promise => { - try { - await lstat(path); - return true; - } catch (cause: unknown) { - if (errorCode(cause) === "ENOENT") return false; - throw cause; - } -}; - -const processIsAlive = (pid: number): boolean => { - try { - process.kill(pid, 0); - return true; - } catch (cause: unknown) { - return errorCode(cause) !== "ESRCH"; - } -}; - -const errorCode = (cause: unknown): unknown => - typeof cause === "object" && cause !== null && "code" in cause - ? cause.code - : undefined; diff --git a/src/application/WebNetworkCapturePort.ts b/src/application/WebNetworkCapturePort.ts new file mode 100644 index 0000000000..5f9cd5545f --- /dev/null +++ b/src/application/WebNetworkCapturePort.ts @@ -0,0 +1,15 @@ +import type { ExecutionOptions } from "./AnalysisProvider.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { Result } from "../domain/result.js"; +import type { + InspectWebNetworkCaptureInput, + WebNetworkCapture, +} from "../domain/webNetworkCapture.js"; + +/** Replaceable offline capture port; producer protocols remain in format adapters. */ +export interface WebNetworkCapturePort { + inspect( + input: InspectWebNetworkCaptureInput, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/WebNetworkCaptureService.test.ts b/src/application/WebNetworkCaptureService.test.ts new file mode 100644 index 0000000000..0105e2d3f9 --- /dev/null +++ b/src/application/WebNetworkCaptureService.test.ts @@ -0,0 +1,340 @@ +import { expect, it } from "vitest"; +import { WebNetworkCaptureService } from "./WebNetworkCaptureService.js"; +import { historicalHar } from "../../tests/fixtures/historicalHar.js"; +import { ok } from "../domain/result.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { parseEvidence } from "../domain/evidence.js"; +import { + webNetworkCaptureSchema, + type WebNetworkCapture, +} from "../domain/webNetworkCapture.js"; + +it.each(["REDACTED", "[", "]", "/capture.har"])( + "excludes sensitive path text even when a replacement marker would collide: %s", + async (literal) => { + const report = fixture(); + report.artifact.path = `/capture.har/${literal}`; + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(report)), + }).inspect({ + capture_path: report.artifact.path, + format: "har", + sensitive_values: [literal], + }); + if (!result.ok) throw result.error; + expect(result.value.subject?.local_path).not.toContain(literal); + expect(result.value.parameters.capture_path).not.toContain(literal); + expect( + result.value.locations.every( + (location) => + location.kind !== "artifact-path" || !location.path.includes(literal), + ), + ).toBe(true); + expect(result.value.subject?.digest.sha256).toBe(report.artifact.sha256); + }, +); + +it("keeps observed artifact identity when the entire sensitive path is excluded", async () => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: ["/", "[", "REDACTED"], + }); + if (!result.ok) throw result.error; + expect(result.value.subject).toMatchObject({ + local_path: "", + digest: { sha256: "a".repeat(64) }, + }); + expect(result.value.locations).toEqual([]); +}); + +const fixture = (): WebNetworkCapture => ({ + decoder: { id: "test-historical-port", name: "test port", version: "1" }, + container: { + reported: null, + numeric_literals: [], + redactions: [], + records_pointer: "/log/entries", + }, + total_records: 1, + records: [ + { + ordinal: 0, + location: { kind: "json-pointer", pointer: "/log/entries/0" }, + reported: historicalHar().log.entries[0] ?? null, + numeric_literals: [], + binary_fields: [], + redactions: [], + limitations: [], + }, + ], + artifact: { path: "/capture.har", sha256: "a".repeat(64), bytes: 10 }, + format: "har", + runtime_attribution: "unknown", + limitations: ["historical fixture"], +}); + +it.each(["a".repeat(64), "aaaa"])( + "marks an explicitly sensitive artifact digest unknown without inventing another identity: %s", + async (literal) => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: [literal], + }); + if (!result.ok) throw result.error; + expect(result.value.subject).toBeNull(); + expect(result.value.normalized_result).toMatchObject({ + artifact: { path: "/capture.har", sha256: null, bytes: 10 }, + }); + expect(result.value.parameters.capture_path).toBe("/capture.har"); + expect(result.value.locations).toEqual([ + { kind: "artifact-path", path: "/capture.har" }, + ]); + expect(parseEvidence(result.value)).toEqual(result.value); + }, +); + +it("projects the unknown-subject explanation before generating its Evidence ID", async () => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: ["a"], + }); + if (!result.ok) throw result.error; + expect(result.value.subject).toBeNull(); + expect(result.value.limitations.every((text) => !text.includes("a"))).toBe( + true, + ); + expect(parseEvidence(result.value)).toEqual(result.value); +}); + +it("rejects an adapter omitting its original observed digest before caller projection", async () => { + const report = fixture(); + report.artifact.sha256 = null; + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(report)), + }).inspect({ capture_path: report.artifact.path, format: "har" }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError"); +}); + +it.each([ + ["producer"], + ["artifact"], + ["coordinates"], + ["producer", "REDACTED", "…"], + ["producer", "[", "]"], +])( + "excludes declared literals from all authored limitations: %j", + async (...literals) => { + const report = fixture(); + report.artifact.path = "/artifact/capture.har"; + report.limitations = [ + "Historical producer artifact coordinates are retained.", + ]; + const first = report.records[0]; + if (first === undefined) throw new Error("fixture missing record"); + first.limitations = ["This is producer-decoded artifact evidence."]; + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(report)), + }).inspect({ + capture_path: report.artifact.path, + format: "har", + sensitive_values: literals, + }); + if (!result.ok) throw result.error; + const projected = webNetworkCaptureSchema.parse( + result.value.normalized_result, + ); + const texts = [ + result.value.limitations, + projected.limitations, + ...projected.records.map((record) => record.limitations), + ].flat(); + for (const literal of literals) + expect(texts.every((text) => !text.includes(literal))).toBe(true); + expect(result.value.subject?.digest.sha256).toBe(report.artifact.sha256); + expect(projected.decoder).toEqual(report.decoder); + expect(projected.records[0]?.reported).toEqual(first.reported); + }, +); + +it("keeps explicitly sensitive values out of Evidence parameters and preserves historical authority", async () => { + const service = new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }); + const result = await service.inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: ["explicit-private-value"], + }); + if (!result.ok) throw result.error; + expect(result.value.parameters).toMatchObject({ + sensitive_value_count: 1, + record_ordinals: [0], + }); + expect(JSON.stringify(result.value)).not.toContain("explicit-private-value"); + expect(result.value.authority).toBe("historical-reference"); + expect(result.value.provider).toEqual(fixture().decoder); +}); + +it.each(["har", "ha"])( + "omits a format parameter containing a declared sensitive literal: %s", + async (literal) => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: [literal], + }); + if (!result.ok) throw result.error; + expect(result.value.parameters).not.toHaveProperty("format"); + expect(JSON.stringify(result.value.parameters)).not.toContain(literal); + expect(result.value.normalized_result).toMatchObject({ format: "har" }); + }, +); + +it.each([ + { record_ordinals: [0, 0] }, + { record_ordinals: [1] }, + { capture_path: "relative.har" }, +])("rejects invalid caller selection %j", async (extra) => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ capture_path: "/capture.har", format: "har", ...extra }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError"); +}); + +it("rejects a port changing the artifact identity or producer ordinal sequence", async () => { + const report = fixture(); + report.artifact.path = "/different.har"; + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(report)), + }).inspect({ capture_path: "/capture.har", format: "har" }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError"); +}); + +it("applies an explicit sensitive path literal after verifying the selected artifact", async () => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: ["capture.har"], + }); + if (!result.ok) throw result.error; + expect(JSON.stringify(result.value)).not.toContain("capture.har"); + expect(result.value.parameters.capture_path).toBe(""); +}); + +it.each(["log", "entries"])( + "excludes a sensitive producer coordinate without inventing a replacement: %s", + async (literal) => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: [literal], + }); + if (!result.ok) throw result.error; + expect(result.value.normalized_result).toMatchObject({ + container: { records_pointer: null }, + records: [ + { + ordinal: 0, + location: { kind: "unknown", reason: "explicit-sensitive-value" }, + }, + ], + }); + expect(JSON.stringify(result.value)).not.toContain(literal); + }, +); + +it("excludes an escaped sensitive sidecar coordinate while preserving its reported field", async () => { + const report = fixture(); + report.container.reported = { "field/name": 42 }; + report.container.numeric_literals = [ + { pointer: "/field~1name", producer_type: "json-number", literal: "42" }, + ]; + report.container.redactions = [ + { pointer: "/field~1name", reason: "explicit-sensitive-value" }, + ]; + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(report)), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: ["~1"], + }); + if (!result.ok) throw result.error; + expect(result.value.normalized_result).toMatchObject({ + container: { + reported: { "field/name": 42 }, + numeric_literals: [], + redactions: [], + }, + }); + expect(JSON.stringify(result.value)).not.toContain("~1"); +}); + +it.each([ + { literal: "12345", input: { record_ordinals: [12345] } }, + { literal: "Each", input: { record_ordinals: [0, 0] } }, + { literal: "absolute", input: { capture_path: "relative.har" } }, + { literal: "secret", input: { mysecret: true } }, +])( + "excludes declared text from application-generated input errors: $literal", + async ({ literal, input }) => { + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(fixture())), + }).inspect({ + capture_path: "/capture.har", + format: "har", + sensitive_values: [literal], + ...input, + }); + if (result.ok) throw new Error("Expected invalid selection"); + expect(result.error._tag).toBe("AnalysisInputError"); + expect(JSON.stringify(projectAnalysisError(result.error))).not.toContain( + literal, + ); + }, +); + +it.each(["12345", "67890", "234", "789"])( + "excludes sensitive native byte-range coordinates: %s", + async (literal) => { + const report = fixture(); + report.format = "mitmproxy"; + report.artifact.path = "/capture.mitm"; + const record = report.records[0]; + if (record === undefined) throw new Error("record missing"); + record.location = { kind: "byte-range", offset: 12345, bytes: 67890 }; + const result = await new WebNetworkCaptureService({ + inspect: () => Promise.resolve(ok(report)), + }).inspect({ + capture_path: report.artifact.path, + format: "mitmproxy", + sensitive_values: [literal], + }); + if (!result.ok) throw result.error; + expect(result.value.normalized_result).toMatchObject({ + records: [ + { location: { kind: "unknown", reason: "explicit-sensitive-value" } }, + ], + }); + expect(result.value.raw_result).toBeNull(); + expect(parseEvidence(result.value)).toEqual(result.value); + }, +); diff --git a/src/application/WebNetworkCaptureService.ts b/src/application/WebNetworkCaptureService.ts new file mode 100644 index 0000000000..ca86b72880 --- /dev/null +++ b/src/application/WebNetworkCaptureService.ts @@ -0,0 +1,202 @@ +import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js"; +import { excludeCaptureCoordinates } from "../domain/webNetworkCaptureCoordinates.js"; +import { redactExplicitText } from "../domain/explicitSensitiveValues.js"; +import { isAbsolute } from "node:path"; +import type { ExecutionOptions } from "./AnalysisProvider.js"; +import type { WebNetworkCapturePort } from "./WebNetworkCapturePort.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../domain/analysisErrorCore.js"; +import { createEvidence, type Evidence } from "../domain/evidence.js"; +import { projectInputIssues } from "../domain/inputIssueProjection.js"; +import { jsonObjectSchema } from "../domain/jsonValue.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { + inspectWebNetworkCaptureInputSchema, + webNetworkCaptureSchema, +} from "../domain/webNetworkCapture.js"; + +const OPERATION = "inspect_web_network_capture"; + +/** Shared CLI/MCP historical inspection preserves caller selection and producer record identity. */ +export class WebNetworkCaptureService { + constructor(readonly port: WebNetworkCapturePort) {} + + /** Return selected complete records inline, without inventing live browser attribution. */ + async inspect( + rawInput: unknown, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION)); + const declarations = + inspectWebNetworkCaptureInputSchema.shape.sensitive_values.safeParse( + typeof rawInput === "object" && + rawInput !== null && + "sensitive_values" in rawInput + ? rawInput.sensitive_values + : undefined, + ); + const failure = (error: AnalysisError): Result => + err( + redactExplicitFailure( + error, + declarations.success ? declarations.data : [], + ), + ); + const parsed = inspectWebNetworkCaptureInputSchema.safeParse(rawInput); + if (!parsed.success) + return failure( + new AnalysisInputError( + OPERATION, + { cause: parsed.error }, + projectInputIssues(parsed.error.issues, rawInput), + ), + ); + const input = parsed.data; + if (!isAbsolute(input.capture_path)) + return failure( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["capture_path"], + reason: "invalid_format", + message: "Expected an absolute filesystem path on this host.", + }, + ]), + ); + if ( + input.record_ordinals !== undefined && + new Set(input.record_ordinals).size !== input.record_ordinals.length + ) + return failure( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["record_ordinals"], + reason: "invalid_format", + message: "Each requested producer ordinal must occur once.", + }, + ]), + ); + const loaded = await this.port.inspect(input, options); + if (!loaded.ok) return failure(loaded.error); + if (options?.signal?.aborted) + return failure(new AnalysisCancelledError(OPERATION)); + const report = webNetworkCaptureSchema.safeParse(loaded.value); + if (!report.success) + return failure( + new AnalysisOutputError( + OPERATION, + "Capture adapter returned malformed historical evidence.", + ), + ); + const value = report.data; + const artifactSha256 = value.artifact.sha256; + if (artifactSha256 === null) + return failure( + new AnalysisOutputError( + OPERATION, + "Capture adapter omitted its observed artifact digest before explicit projection.", + ), + ); + if ( + value.artifact.path !== input.capture_path || + value.format !== input.format || + value.total_records !== value.records.length || + value.records.some((record, index) => record.ordinal !== index) + ) + return failure( + new AnalysisOutputError( + OPERATION, + "Capture adapter changed the selected artifact, format or original record sequence.", + ), + ); + const safeValue = excludeCaptureCoordinates(value, input.sensitive_values); + const redactText = (text: string): string => + redactExplicitText(text, input.sensitive_values); + const selected = + input.record_ordinals ?? value.records.map(({ ordinal }) => ordinal); + const records = []; + for (const ordinal of selected) { + const record = safeValue.records[ordinal]; + if (record === undefined) + return failure( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["record_ordinals"], + reason: "invalid_format", + message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`, + }, + ]), + ); + records.push({ + ...record, + limitations: record.limitations.map(redactText), + }); + } + const safePath = input.sensitive_values.some((literal) => + input.capture_path.includes(literal), + ) + ? "" + : input.capture_path; + const safeSha256 = input.sensitive_values.some((literal) => + artifactSha256.includes(literal), + ) + ? null + : artifactSha256; + const subjectUnavailableReason = redactText( + "The explicitly sensitive artifact digest is excluded; its cryptographic identity is unknown in this Evidence.", + ); + const limitations = [ + ...safeValue.limitations, + ...(safePath === "" + ? [ + "The explicitly sensitive artifact path is excluded; its size remains available.", + ] + : []), + ].map(redactText); + const parameters = jsonObjectSchema.parse({ + capture_path: safePath, + ...(input.sensitive_values.some((literal) => + input.format.includes(literal), + ) + ? {} + : { format: input.format }), + record_ordinals: selected, + sensitive_value_count: input.sensitive_values.length, + }); + return ok( + createEvidence( + safeSha256 === null + ? undefined + : { + path: safePath, + sha256: safeSha256, + format: "file", + }, + value.decoder, + { + operation: OPERATION, + parameters, + result: { + ...safeValue, + artifact: { ...value.artifact, path: safePath, sha256: safeSha256 }, + records, + limitations: + safeSha256 === null + ? [...limitations, subjectUnavailableReason] + : limitations, + }, + confidence: "observed", + authority: "historical-reference", + limitations, + ...(safeSha256 === null ? { subjectUnavailableReason } : {}), + locations: + safePath === "" ? [] : [{ kind: "artifact-path", path: safePath }], + }, + ), + ); + } +} diff --git a/src/application/WindowsCapabilities.ts b/src/application/WindowsCapabilities.ts index 3e95bed1d9..dfd092c69c 100644 --- a/src/application/WindowsCapabilities.ts +++ b/src/application/WindowsCapabilities.ts @@ -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 = diff --git a/src/application/ArtifactExtractionDestination.ts b/src/application/artifacts/ArtifactExtractionDestination.ts similarity index 100% rename from src/application/ArtifactExtractionDestination.ts rename to src/application/artifacts/ArtifactExtractionDestination.ts diff --git a/src/application/binary/AnalysisProviderRegistry.test.ts b/src/application/binary/AnalysisProviderRegistry.test.ts index acbbae5fc7..8c0b2ed4e2 100644 --- a/src/application/binary/AnalysisProviderRegistry.test.ts +++ b/src/application/binary/AnalysisProviderRegistry.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - AnalysisProviderRegistry, - type AnalysisProviderBinding, -} from "./AnalysisProviderRegistry.js"; +import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js"; import { createAnalysisExecution, type AnalysisClient, @@ -33,132 +30,77 @@ const ARTIFACT_TARGET: BinaryTarget = { format: "asar", }; -describe("analysis provider registry: discovery and selection", () => { - it("admits overlapping operations, sorts discovery, and rejects duplicate IDs", () => { - const alpha = candidate("alpha"); - const beta = candidate("beta"); - const registry = new AnalysisProviderRegistry([ - beta.provider, - alpha.provider, - ]); +it("rejects malformed provider capability declarations", () => { + const alpha = candidate("alpha"); + expect( + () => new AnalysisProviderRegistry([alpha.provider, alpha.provider]), + ).toThrow(/Duplicate analysis provider ID: alpha/u); + expect( + () => + new AnalysisProviderRegistry([ + { + ...alpha.provider, + capabilities: () => { + const descriptor = capability(alpha.provider.identity()); + return [descriptor, descriptor]; + }, + }, + ]), + ).toThrow(/declares operation address_name more than once/u); + expect( + () => + new AnalysisProviderRegistry([ + { + ...alpha.provider, + capabilities: () => [ + capability({ ...alpha.provider.identity(), name: "forged" }), + ], + }, + ]), + ).toThrow(/published mismatched capability provenance/u); + expect( + () => new AnalysisProviderRegistry([candidate("auto").provider]), + ).toThrow(/Invalid analysis provider ID: auto/u); +}); - expect(registry.identities().map(({ id }) => id)).toEqual([ - "alpha", - "beta", - ]); - expect( - registry.candidates().map(({ provider, targetSupport }) => ({ - id: provider.id, - support: targetSupport.status, - })), - ).toEqual([ - { id: "alpha", support: "unknown" }, - { id: "beta", support: "unknown" }, - ]); - expect(alpha.created).toEqual([]); - expect(beta.created).toEqual([]); - expect( - () => new AnalysisProviderRegistry([alpha.provider, alpha.provider]), - ).toThrow(/Duplicate analysis provider ID: alpha/u); - expect( - () => - new AnalysisProviderRegistry([ - { - ...alpha.provider, - capabilities: () => { - const descriptor = capability(alpha.provider.identity()); - return [descriptor, descriptor]; - }, - }, - ]), - ).toThrow(/declares operation address_name more than once/u); - expect( - () => - new AnalysisProviderRegistry([ - { - ...alpha.provider, - capabilities: () => [ - capability({ - ...alpha.provider.identity(), - name: "mismatched provider", - }), - ], - }, - ]), - ).toThrow(/published mismatched capability provenance/u); - expect( - () => new AnalysisProviderRegistry([candidate("auto").provider]), - ).toThrow(/Invalid analysis provider ID: auto/u); +it("selects the sole available provider and honors explicit selection precedence", async () => { + const alpha = candidate("alpha"); + const beta = candidate("beta", { available: false }); + const sole = await new AnalysisProviderRegistry([ + beta.provider, + alpha.provider, + ]).select(DATABASE_TARGET); + expect(sole).toMatchObject({ + ok: true, + value: { + binding: { + identity: { id: "alpha" }, + selectionSource: "auto-single-candidate", + }, + }, }); - it("selects the sole usable candidate without creating a client", async () => { - const alpha = candidate("alpha"); - const beta = candidate("beta", { available: false }); - const registry = new AnalysisProviderRegistry([ - beta.provider, - alpha.provider, - ]); - - const selected = await registry.select(DATABASE_TARGET); - expect(selected.ok).toBe(true); - if (!selected.ok) throw new Error("provider selection failed"); - if (selected.value.binding === null) - throw new Error("expected a bound provider"); - expect(bindingProjection(selected.value.binding)).toEqual({ - id: "alpha", - source: "auto-single-candidate", - version: "1", - }); - expect(selected.value.candidates).toMatchObject([ - { provider: { id: "alpha", version: "1" }, selected: true }, - { - provider: { id: "beta" }, - selected: false, - availability: { status: "unavailable", code: "runtime_missing" }, + const selected = new AnalysisProviderRegistry( + [alpha.provider, candidate("beta").provider], + "beta", + ); + expect(await selected.select(DATABASE_TARGET)).toMatchObject({ + ok: true, + value: { + binding: { + identity: { id: "beta" }, + selectionSource: "environment", }, - ]); - expect(alpha.created).toEqual([]); - expect(beta.created).toEqual([]); + }, }); - - it("makes auto ambiguity explicit and honors request over environment", async () => { - const alpha = candidate("alpha"); - const beta = candidate("beta"); - const registry = new AnalysisProviderRegistry( - [alpha.provider, beta.provider], - "beta", - ); - - const environment = await registry.select(DATABASE_TARGET); - expect(environment.ok).toBe(true); - if (environment.ok) - expect(environment.value.binding?.selectionSource).toBe("environment"); - expect(environment.ok && environment.value.binding?.identity.id).toBe( - "beta", - ); - - const request = await registry.select(DATABASE_TARGET, "alpha"); - expect(request.ok && request.value.binding?.identity.id).toBe("alpha"); - if (request.ok) - expect(request.value.binding?.selectionSource).toBe("request"); - - const automatic = await registry.select(DATABASE_TARGET, "auto"); - expect(automatic.ok).toBe(false); - if (automatic.ok) return; - expect(automatic.error).toMatchObject({ - reason: "ambiguous", - requestedProviderId: "auto", - candidateIds: ["alpha", "beta"], - }); - expect(projectAnalysisError(automatic.error)).toMatchObject({ - code: "capability_unavailable", - details: { - selection_reason: "ambiguous", - candidate_ids: ["alpha", "beta"], + expect(await selected.select(DATABASE_TARGET, "alpha")).toMatchObject({ + ok: true, + value: { + binding: { + identity: { id: "alpha" }, + selectionSource: "request", }, - }); - expect(alpha.created).toEqual([]); - expect(beta.created).toEqual([]); + }, }); }); @@ -414,9 +356,3 @@ const capability = (provider: ProviderIdentity): CapabilityDescriptor => ({ }, limitations: [], }); - -const bindingProjection = (binding: AnalysisProviderBinding) => ({ - id: binding.identity.id, - source: binding.selectionSource, - version: binding.identity.version, -}); diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 79ccfaa735..5a7472ba4e 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -10,15 +10,137 @@ import { createEvidence } from "../../domain/evidence.js"; import { createEvidenceBundle } from "../../domain/evidenceBundle.js"; import { ok } from "../../domain/result.js"; import { createAnalysisExecution } from "../AnalysisProvider.js"; +import { + createAnalysisSnapshotWorkflowEntry, + parseAnalysisSnapshot, + snapshotBinding, + snapshotTarget, +} from "../../domain/analysisSnapshot.js"; +import { + REA_WORKFLOW_PROVIDER, + workflowAnalysisProfile, +} from "../InvestigationProviders.js"; import type { AnalysisOperation, CapabilityDescriptor, } from "../AnalysisProvider.js"; import { AnalysisSnapshotCache, + SNAPSHOT_CACHE_ENTRY_CEILING, isSnapshotCacheable, } from "./AnalysisSnapshotCache.js"; +describe("analysis snapshot cache capacity", () => { + it("shares capacity across query kinds while allowing replacements and fresh partitions", () => { + const cache = new AnalysisSnapshotCache(); + const target = ANALYSIS_SNAPSHOT_TARGET; + const profile = ANALYSIS_SNAPSHOT_PROFILE; + const execution = { + ...createAnalysisExecution("workflow", REA_WORKFLOW_PROVIDER, { + subject: target, + }), + analysisProfile: workflowAnalysisProfile(profile), + }; + const workflowInput = { + target, + profile, + operation: "trace_feature", + parameters: { query: "existing" }, + execution, + }; + for (let index = 0; index < SNAPSHOT_CACHE_ENTRY_CEILING - 1; index += 1) + cache.recordWorkflow({ + ...workflowInput, + parameters: { query: String(index) }, + }); + // Staging and recording both spend the same budget; an existing query can + // still be refreshed when no space remains for a new binding. + const directInput = { + target, + profile, + operation: "address_name" as const, + parameters: { address: "0x1000", document: "fixture" }, + execution: createAnalysisExecution( + "initial", + ANALYSIS_SNAPSHOT_PROVIDER, + { subject: target, analysisProfile: profile }, + ), + }; + cache.record(directInput); + cache.record({ + ...directInput, + parameters: { address: "0x2000", document: "fixture" }, + }); + cache.recordWorkflow(workflowInput); + expect(cache.entries()).toHaveLength(1); + expect(cache.workflowEntries()).toHaveLength( + SNAPSHOT_CACHE_ENTRY_CEILING - 1, + ); + + cache.record({ + ...directInput, + execution: { ...directInput.execution, result: "updated" }, + }); + cache.recordWorkflow({ + ...workflowInput, + parameters: { query: "0" }, + execution: { ...execution, result: "updated" }, + }); + expect( + cache.lookup(target, profile, "address_name", directInput.parameters) + ?.result, + ).toBe("updated"); + expect( + cache.workflowEntries().find(({ parameters }) => parameters.query === "0") + ?.execution.result, + ).toBe("updated"); + + const incoming = createAnalysisSnapshotWorkflowEntry({ + ...workflowInput, + target: snapshotTarget(target), + binding: snapshotBinding(profile), + }); + const evidence = createEvidence(target, REA_WORKFLOW_PROVIDER, { + operation: workflowInput.operation, + parameters: workflowInput.parameters, + result: execution.result, + rawResult: execution.rawResult, + analysisProfile: execution.analysisProfile, + limitations: execution.limitations, + locations: execution.locations, + }); + cache.stage( + parseAnalysisSnapshot({ + target: snapshotTarget(target), + binding: snapshotBinding(profile), + entries: [], + workflow_entries: [incoming], + evidence_bundle: createEvidenceBundle([evidence]), + }), + ); + expect(cache.workflowEntries()).toHaveLength( + SNAPSHOT_CACHE_ENTRY_CEILING - 1, + ); + + const otherTarget = { ...target, sha256: "b".repeat(64) }; + cache.recordWorkflow({ + ...workflowInput, + target: otherTarget, + execution: { ...execution, subject: otherTarget }, + }); + expect(cache.entries()).toEqual([]); + expect(cache.workflowEntries()).toHaveLength(1); + cache.clear(); + cache.record({ + ...directInput, + target: otherTarget, + execution: { ...directInput.execution, subject: otherTarget }, + }); + expect(cache.entries()).toHaveLength(1); + expect(cache.workflowEntries()).toEqual([]); + }); +}); + describe("analysis snapshot cache partitioning", () => { it.each<{ operation: Exclude; diff --git a/src/application/binary/AnalysisSnapshotCache.ts b/src/application/binary/AnalysisSnapshotCache.ts index 4734345425..edb39da10c 100644 --- a/src/application/binary/AnalysisSnapshotCache.ts +++ b/src/application/binary/AnalysisSnapshotCache.ts @@ -9,6 +9,7 @@ import { err, ok, type Result } from "../../domain/result.js"; import { analysisQueryId, createAnalysisSnapshotEntry, + createAnalysisSnapshotWorkflowEntry, snapshotBinding, snapshotMatchesProfile, snapshotMatchesTarget, @@ -16,6 +17,7 @@ import { parseAnalysisSnapshot, type AnalysisSnapshot, type AnalysisSnapshotEntry, + type AnalysisSnapshotWorkflowEntry, type AnalysisSnapshotBinding, type AnalysisSnapshotTarget, } from "../../domain/analysisSnapshot.js"; @@ -54,6 +56,9 @@ const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet = new Set( ), ); +/** Maximum duplicated result bindings retained across direct and workflow queries. */ +export const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000; + /** Whether an operation is immutable and independent of provider UI state. */ export const isSnapshotCacheable = ( operation: AnalysisOperation, @@ -73,6 +78,7 @@ export const isSnapshotCacheable = ( /** Bounded in-memory cache for one immutable binary identity. */ export class AnalysisSnapshotCache { readonly #entries = new Map(); + readonly #workflowEntries = new Map(); #target: AnalysisSnapshotTarget | undefined; #binding: AnalysisSnapshotBinding | undefined; @@ -93,7 +99,7 @@ export class AnalysisSnapshotCache { /** Replace cache state when the target or selected profile changes. */ select(target: BinaryTarget, profile: AnalysisProfileCommitment): void { - if (!this.matches(target, profile)) this.#entries.clear(); + if (!this.matches(target, profile)) this.#clearEntries(); this.#target = snapshotTarget(target); this.#binding = snapshotBinding(profile); } @@ -109,14 +115,20 @@ export class AnalysisSnapshotCache { snapshot.binding.analysis_profile, )) ) - this.#entries.clear(); + this.#clearEntries(); this.#target = structuredClone(snapshot.target); this.#binding = structuredClone(snapshot.binding); let imported = 0; for (const entry of snapshot.entries) { + if (!this.#entries.has(entry.query_id) && !this.#hasCapacity()) continue; if (!this.#entries.has(entry.query_id)) imported += 1; this.#entries.set(entry.query_id, structuredClone(entry)); } + for (const entry of snapshot.workflow_entries ?? []) { + if (!this.#workflowEntries.has(entry.query_id) && !this.#hasCapacity()) + continue; + this.#workflowEntries.set(entry.query_id, structuredClone(entry)); + } return imported; } @@ -137,6 +149,9 @@ export class AnalysisSnapshotCache { target: snapshotTargetIdentity, binding, entries: this.entries(), + ...(this.workflowEntries().length === 0 + ? {} + : { workflow_entries: this.workflowEntries() }), evidence_bundle: retainedEvidence, }), ); @@ -193,6 +208,47 @@ export class AnalysisSnapshotCache { .map((entry) => structuredClone(entry)); } + /** Return canonical composed-workflow entries for persistence. */ + workflowEntries(): AnalysisSnapshotWorkflowEntry[] { + return [...this.#workflowEntries.values()] + .sort((left, right) => left.query_id.localeCompare(right.query_id)) + .map((entry) => structuredClone(entry)); + } + + /** Record one exact derived workflow result unless the shared cache is full. */ + recordWorkflow(input: { + readonly target: BinaryTarget; + readonly profile: AnalysisProfileCommitment; + readonly operation: string; + readonly parameters: Readonly>; + readonly execution: { + readonly result: JsonValue; + readonly rawResult: JsonValue | null; + readonly provider: AnalysisExecution["provider"]; + readonly analysisProfile: AnalysisProfileCommitment; + readonly limitations: readonly string[]; + readonly locations: AnalysisExecution["locations"]; + readonly subject: AnalysisExecution["subject"]; + }; + }): void { + this.select(input.target, input.profile); + const queryId = analysisQueryId( + snapshotTarget(input.target), + snapshotBinding(input.profile), + input.operation, + input.parameters, + ); + if (!this.#workflowEntries.has(queryId) && !this.#hasCapacity()) return; + const entry = createAnalysisSnapshotWorkflowEntry({ + target: snapshotTarget(input.target), + binding: snapshotBinding(input.profile), + operation: input.operation, + parameters: input.parameters, + execution: input.execution, + }); + this.#workflowEntries.set(entry.query_id, entry); + } + /** Replay an exact provider-specific query, marking its cached provenance. */ lookup( target: BinaryTarget, @@ -246,8 +302,14 @@ export class AnalysisSnapshotCache { readonly execution: AnalysisExecution; }): void { const { target, profile, operation, parameters, execution } = input; - if (this.#entries.size >= 10_000) return; this.select(target, profile); + const queryId = analysisQueryId( + snapshotTarget(target), + snapshotBinding(profile), + operation, + parameters, + ); + if (!this.#entries.has(queryId) && !this.#hasCapacity()) return; const entry = createAnalysisSnapshotEntry({ target: snapshotTarget(target), binding: snapshotBinding(profile), @@ -260,8 +322,20 @@ export class AnalysisSnapshotCache { /** Forget all target-bound entries. */ clear(): void { - this.#entries.clear(); + this.#clearEntries(); this.#target = undefined; this.#binding = undefined; } + + #clearEntries(): void { + this.#entries.clear(); + this.#workflowEntries.clear(); + } + + #hasCapacity(): boolean { + return ( + this.#entries.size + this.#workflowEntries.size < + SNAPSHOT_CACHE_ENTRY_CEILING + ); + } } diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index 7acc843573..fa01de1246 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -30,7 +30,10 @@ import { BinarySessionRecords } from "./BinarySessionRecords.js"; import { binarySessionStatus } from "./BinarySessionStatus.js"; import { resolveSessionOpen, + resolveSessionTarget, + validateResolvedSessionTarget, type BinarySessionOpenOptions, + type ResolvedSessionOpen, } from "./BinarySessionOpen.js"; import { bindExecutionTarget, @@ -150,6 +153,27 @@ export class BinarySession return () => this.#availabilityListeners.delete(listener); } + /** Resolve a target and provider profile without creating a provider client. */ + previewTarget( + target: BinaryTarget, + options: BinarySessionOpenOptions = {}, + ): Promise> { + if (isAborted(options.signal)) + return Promise.resolve(err(new AnalysisCancelledError("open_binary"))); + return resolveSessionTarget({ + router: this.#providerRouter, + current: this.#active, + target, + options, + stagedSnapshotMatches: (target, profile) => + this.matchesSnapshot(target, profile), + }).then((resolved) => + isAborted(options.signal) + ? err(new AnalysisCancelledError("open_binary")) + : resolved, + ); + } + /** * Open or switch targets after draining calls against the current target. * Returns the switch failure even if best-effort restoration also fails. @@ -157,18 +181,49 @@ export class BinarySession open( path: string, options: BinarySessionOpenOptions = {}, + ): Promise> { + return this.#open( + () => + resolveSessionOpen({ + router: this.#providerRouter, + current: this.#active, + path, + options, + stagedSnapshotMatches: (target, profile) => + this.matchesSnapshot(target, profile), + }), + options, + ); + } + + /** Open a previewed target without repeating parsing or provider profile discovery. */ + openResolvedTarget( + resolved: Pick, + options: Pick = {}, + ): Promise> { + return this.#open( + () => + Promise.resolve( + validateResolvedSessionTarget({ + ...resolved, + current: this.#active, + options, + stagedSnapshotMatches: (target, profile) => + this.matchesSnapshot(target, profile), + }), + ), + options, + ); + } + + #open( + resolve: () => Promise>, + options: Pick, ): Promise> { return this.#serialize(async () => { if (isAborted(options.signal)) return err(new AnalysisCancelledError("open_binary")); - const resolved = await resolveSessionOpen({ - router: this.#providerRouter, - current: this.#active, - path, - options, - stagedSnapshotMatches: (target, profile) => - this.matchesSnapshot(target, profile), - }); + const resolved = await resolve(); if (!resolved.ok) return resolved; const { target, route, sameTarget } = resolved.value; const { profile, compatibility } = route; @@ -319,7 +374,8 @@ export class BinarySession lookupSnapshot: (target, profile, operation, parameters) => operation === "decode_interface_builder" || operation === "inspect_asset_catalog" || - operation === "inspect_keyed_archive" + operation === "inspect_keyed_archive" || + operation === "trace_dylib_resolution" ? undefined : this.lookupSnapshot(target, profile, operation, parameters), }); @@ -342,7 +398,8 @@ export class BinarySession profile !== undefined && name !== "decode_interface_builder" && name !== "inspect_asset_catalog" && - name !== "inspect_keyed_archive" + name !== "inspect_keyed_archive" && + name !== "trace_dylib_resolution" ) { const evidence = createEvidence( profiled.value.subject ?? active.target, diff --git a/src/application/binary/BinarySessionOpen.ts b/src/application/binary/BinarySessionOpen.ts index e0d425ee97..859d8f4137 100644 --- a/src/application/binary/BinarySessionOpen.ts +++ b/src/application/binary/BinarySessionOpen.ts @@ -46,11 +46,22 @@ interface ResolveSessionOpenInput { ) => boolean; } +interface ResolveSessionTargetInput { + readonly router: SessionProviderRouter; + readonly current: CurrentOpenBinding | undefined; + readonly target: BinaryTarget; + readonly options: BinarySessionOpenOptions; + readonly stagedSnapshotMatches: ( + target: BinaryTarget, + profile: AnalysisProfileCommitment | null, + ) => boolean; +} + /** Parse a target, resolve its provider route, and validate snapshot binding. */ export const resolveSessionOpen = async ( input: ResolveSessionOpenInput, ): Promise> => { - const { router, current, path, options, stagedSnapshotMatches } = input; + const { path, options } = input; const parsed = await parseBinaryTarget( path, process.cwd(), @@ -59,7 +70,14 @@ export const resolveSessionOpen = async ( options.formatHint, ); if (!parsed.ok) return parsed; - const target = parsed.value; + return resolveSessionTarget({ ...input, target: parsed.value }); +}; + +/** Resolve a known target's provider route and validate snapshot binding. */ +export const resolveSessionTarget = async ( + input: ResolveSessionTargetInput, +): Promise> => { + const { router, current, target, options } = input; const sameTarget = current?.target.path === target.path && snapshotMatchesTarget(snapshotTarget(current.target), target); @@ -68,7 +86,22 @@ export const resolveSessionOpen = async ( ? ({ ok: true, value: current.route } as const) : await router.resolve(target, options.providerId, options.signal); if (!resolvedRoute.ok) return resolvedRoute; - const route = resolvedRoute.value; + return validateResolvedSessionTarget({ + ...input, + route: resolvedRoute.value, + }); +}; + +/** Revalidate a previewed route against the current session and snapshot binding. */ +export const validateResolvedSessionTarget = ( + input: Omit & { + readonly route: SessionProviderRoute; + }, +): Result => { + const { current, target, route, options, stagedSnapshotMatches } = input; + const sameTarget = + current?.target.path === target.path && + snapshotMatchesTarget(snapshotTarget(current.target), target); const snapshotError = validateSnapshot({ snapshot: options.snapshot, target, diff --git a/src/application/binary/BinarySessionRecords.ts b/src/application/binary/BinarySessionRecords.ts index 3ec801bda8..ee97d88492 100644 --- a/src/application/binary/BinarySessionRecords.ts +++ b/src/application/binary/BinarySessionRecords.ts @@ -68,7 +68,8 @@ export abstract class BinarySessionRecords { ): Result { const imported = this.#records.mergeEvidenceBundle(bundle); if (!imported.ok) return imported; - if (imported.value.changed) this.#emitSnapshotChanged(); + if (imported.value.metadataChanged) this.invalidateSnapshot(); + else if (imported.value.changed) this.#emitSnapshotChanged(); return ok(imported.value.recordsAdded); } @@ -159,6 +160,43 @@ export abstract class BinarySessionRecords { this.#emitSnapshotChanged(); } + /** Retain one derived workflow result alongside its provider cache entries. */ + recordWorkflowSnapshot(input: { + readonly operation: string; + readonly parameters: Readonly< + Record + >; + readonly execution: Parameters< + AnalysisSnapshotCache["recordWorkflow"] + >[0]["execution"]; + }): Result { + const active = this.activeAnalysisBinding(); + if (active === undefined || active.profile === null) + return err( + new EvidenceIntegrityError( + "Workflow snapshot entries require an active concrete provider profile", + ), + ); + try { + this.#snapshot.recordWorkflow({ + target: active.target, + profile: active.profile, + ...input, + }); + this.#emitSnapshotChanged(); + return ok(null); + } catch (cause: unknown) { + return err( + new EvidenceIntegrityError( + cause instanceof Error + ? cause.message + : "Workflow snapshot entry validation failed", + { cause }, + ), + ); + } + } + protected invalidateSnapshot(): void { this.#snapshot.clear(); this.#snapshotInvalidated = true; diff --git a/src/application/binaryDiagnostics/BinaryLayoutPort.ts b/src/application/binaryDiagnostics/BinaryLayoutPort.ts new file mode 100644 index 0000000000..a6936a60f0 --- /dev/null +++ b/src/application/binaryDiagnostics/BinaryLayoutPort.ts @@ -0,0 +1,17 @@ +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { + BinaryLayout, + InspectBinaryLayoutInput, +} from "../../domain/native/binaryLayout.js"; +import type { Result } from "../../domain/result.js"; +import type { ProviderIdentity } from "../AnalysisProvider.js"; + +/** Replaceable offline binary-layout provider; owns its parser and process protocol. */ +export interface BinaryLayoutPort { + readonly identity: ProviderIdentity; + inspect( + input: InspectBinaryLayoutInput, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts new file mode 100644 index 0000000000..dcfad23111 --- /dev/null +++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts @@ -0,0 +1,131 @@ +import { expect, it } from "vitest"; +import { BinaryLayoutService } from "./BinaryLayoutService.js"; +import { + binaryLayoutFixture, + BINARY_LAYOUT_TEST_PROVIDER, +} from "../../../tests/fixtures/binaryDiagnostics/layout.js"; +import { parseEvidence } from "../../domain/evidence.js"; +import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; +import { err, ok } from "../../domain/result.js"; + +it("binds original bytes, inline observations, unknowns and producing identity", async () => { + const fixture = binaryLayoutFixture(); + const service = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(fixture)), + }); + const result = await service.inspect({ path: fixture.artifact.path }); + if (!result.ok) throw result.error; + const evidence = parseEvidence(result.value); + expect(evidence.normalized_result).toEqual(fixture); + expect(evidence.raw_result).toEqual(fixture); + expect(evidence.subject?.digest.sha256).toBe(fixture.artifact.sha256); + expect(evidence.provider).toEqual(BINARY_LAYOUT_TEST_PROVIDER); + expect(evidence.confidence).toBe("observed"); +}); + +it.each([ + { path: "relative.elf" }, + { path: "/selected.elf", approval: true }, + { path: 123 }, + {}, +])( + "rejects malformed selection before starting the provider: %j", + async (input) => { + const service = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => { + throw new Error("must not run"); + }, + }); + expect(await service.inspect(input)).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); + }, +); + +it("preserves a provider's actionable unsupported reason", async () => { + const failure = new AnalysisCapabilityUnavailableError( + "fixture-layout", + "inspect_binary_layout", + "ELF core belongs to recorded-crash analysis.", + ); + const service = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(err(failure)), + }); + expect(await service.inspect({ path: "/source.elf" })).toEqual(err(failure)); +}); + +it("cancels before invocation and after an acquired observation", async () => { + const controller = new AbortController(); + const service = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => { + controller.abort(); + return Promise.resolve(ok(binaryLayoutFixture())); + }, + }); + expect( + await service.inspect( + { path: "/artifacts/source-owned.elf" }, + { signal: controller.signal }, + ), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCancelledError", + capturedOutput: binaryLayoutFixture().diagnostics, + }, + }); + const untouched = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => { + throw new Error("must not run"); + }, + }); + expect( + await untouched.inspect( + { path: "/source.elf" }, + { signal: controller.signal }, + ), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); +}); + +it.each(["different-path", "invalid-digest", "outside-range"])( + "rejects provider evidence at the next boundary: %s", + async (problem) => { + const fixture = binaryLayoutFixture(); + if (problem === "different-path") fixture.artifact.path = "/different.elf"; + if (problem === "invalid-digest") fixture.artifact.sha256 = "unobserved"; + if (problem === "outside-range") + fixture.segments.push({ + index: 0, + type: "PT_LOAD", + offset: "0x80", + header_location: { offset: "0x0", bytes: "0x38" }, + file_backing: "file", + file_size: "0x1", + memory_size: "0x1", + virtual_address: "0x20000000000001", + physical_address: "0x0", + alignment: "0x1", + flags: "0x5", + permissions: { read: true, write: false, execute: true }, + }); + const service = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(fixture)), + }); + expect( + await service.inspect({ path: "/artifacts/source-owned.elf" }), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisOutputError", + capturedOutput: fixture.diagnostics, + }, + }); + }, +); diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts new file mode 100644 index 0000000000..7fb1d308ea --- /dev/null +++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts @@ -0,0 +1,91 @@ +import { isAbsolute } from "node:path"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import type { BinaryLayoutPort } from "./BinaryLayoutPort.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { + inspectBinaryLayoutInputSchema, + binaryLayoutSchema, +} from "../../domain/native/binaryLayout.js"; +import { projectInputIssues } from "../../domain/inputIssueProjection.js"; +import { err, ok, type Result } from "../../domain/result.js"; + +const OPERATION = "inspect_binary_layout"; + +/** Shared CLI/MCP validation and Evidence composition for offline layout inspection. */ +export class BinaryLayoutService { + constructor(readonly provider: BinaryLayoutPort) {} + + /** Bind complete observations to the caller-selected artifact before creating Evidence. */ + async inspect( + rawInput: unknown, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION)); + const input = inspectBinaryLayoutInputSchema.safeParse(rawInput); + if (!input.success) + return err( + new AnalysisInputError( + OPERATION, + { cause: input.error }, + projectInputIssues(input.error.issues, rawInput), + ), + ); + if (!isAbsolute(input.data.path)) + return err( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["path"], + reason: "invalid_format", + message: "Expected an absolute filesystem path on this host.", + }, + ]), + ); + const inspected = await this.provider.inspect(input.data, options); + if (!inspected.ok) return inspected; + const diagnostics = binaryLayoutSchema.shape.diagnostics.safeParse( + inspected.value.diagnostics, + ); + const outputOptions = diagnostics.success + ? { capturedOutput: diagnostics.data } + : undefined; + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION, outputOptions)); + const report = binaryLayoutSchema.safeParse(inspected.value); + if (!report.success || report.data.artifact.path !== input.data.path) + return err( + new AnalysisOutputError( + OPERATION, + "Binary layout provider returned malformed evidence or changed the selected artifact identity.", + outputOptions, + ), + ); + const value = report.data; + return ok( + createEvidence( + { + path: value.artifact.path, + format: "elf", + architecture: "x86_64", + sha256: value.artifact.sha256, + }, + this.provider.identity, + { + operation: OPERATION, + parameters: input.data, + result: value, + rawResult: value, + confidence: "observed", + limitations: value.limitations, + locations: [{ kind: "artifact-path", path: value.artifact.path }], + }, + ), + ); + } +} diff --git a/src/application/binaryDiagnostics/RecordedCrashPort.ts b/src/application/binaryDiagnostics/RecordedCrashPort.ts new file mode 100644 index 0000000000..3f9551c67d --- /dev/null +++ b/src/application/binaryDiagnostics/RecordedCrashPort.ts @@ -0,0 +1,19 @@ +import type { + ExecutionOptions, + ProviderIdentity, +} from "../AnalysisProvider.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { + InspectRecordedCrashInput, + RecordedCrash, +} from "../../domain/native/recordedCrash.js"; +import type { Result } from "../../domain/result.js"; + +/** Replaceable decoder of supplied recordings; historical PID is never live authority. */ +export interface RecordedCrashPort { + readonly identity: ProviderIdentity; + inspect( + input: InspectRecordedCrashInput, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/binaryDiagnostics/RecordedCrashService.test.ts b/src/application/binaryDiagnostics/RecordedCrashService.test.ts new file mode 100644 index 0000000000..7390f4f31b --- /dev/null +++ b/src/application/binaryDiagnostics/RecordedCrashService.test.ts @@ -0,0 +1,148 @@ +import { expect, it } from "vitest"; +import { RecordedCrashService } from "./RecordedCrashService.js"; +import { + recordedCrashFixture, + RECORDED_CRASH_TEST_PROVIDER, + recordedCrashDebuggerFixture, +} from "../../../tests/fixtures/binaryDiagnostics/recordedCrash.js"; +import { parseEvidence } from "../../domain/evidence.js"; +import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; +import { err, ok } from "../../domain/result.js"; + +it("binds original bytes, inline observations, unknowns and producing identity", async () => { + const fixture = recordedCrashFixture(); + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(fixture)), + }); + const result = await service.inspect({ path: fixture.artifact.path }); + if (!result.ok) throw result.error; + const evidence = parseEvidence(result.value); + expect(evidence.normalized_result).toEqual(fixture); + expect(evidence.raw_result).toEqual(fixture); + expect(evidence.subject?.digest.sha256).toBe(fixture.artifact.sha256); + expect(evidence.provider).toEqual(RECORDED_CRASH_TEST_PROVIDER); + expect(evidence.confidence).toBe("observed"); +}); + +it("labels an envelope containing debugger-derived maps as derived", async () => { + const fixture = recordedCrashFixture(); + fixture.debugger = recordedCrashDebuggerFixture(); + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(fixture)), + }); + const result = await service.inspect({ + path: fixture.artifact.path, + include_debugger_context: true, + }); + if (!result.ok) throw result.error; + const evidence = parseEvidence(result.value); + expect(evidence.confidence).toBe("derived"); + expect(evidence.normalized_result).toEqual(fixture); +}); + +it.each([ + { path: "relative.elf" }, + { path: "/selected.core", approval: true }, + { path: 123 }, + {}, +])( + "rejects malformed selection before starting the provider: %j", + async (input) => { + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => { + throw new Error("must not run"); + }, + }); + expect(await service.inspect(input)).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); + }, +); + +it("preserves a provider's actionable unsupported reason", async () => { + const failure = new AnalysisCapabilityUnavailableError( + "fixture-recorded-crash", + "inspect_recorded_crash", + "ELF core belongs to recorded-crash analysis.", + ); + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => Promise.resolve(err(failure)), + }); + expect(await service.inspect({ path: "/source.elf" })).toEqual(err(failure)); +}); + +it("cancels before invocation and after an acquired observation", async () => { + const controller = new AbortController(); + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => { + controller.abort(); + return Promise.resolve(ok(recordedCrashFixture())); + }, + }); + expect( + await service.inspect( + { path: "/artifacts/source-owned.core" }, + { signal: controller.signal }, + ), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCancelledError", + capturedOutput: recordedCrashFixture().diagnostics, + }, + }); + const untouched = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => { + throw new Error("must not run"); + }, + }); + expect( + await untouched.inspect( + { path: "/source.elf" }, + { signal: controller.signal }, + ), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); +}); + +it.each(["different-path", "invalid-digest", "outside-range"])( + "rejects provider evidence at the next boundary: %s", + async (problem) => { + const fixture = recordedCrashFixture(); + if (problem === "different-path") fixture.artifact.path = "/different.elf"; + if (problem === "invalid-digest") fixture.artifact.sha256 = "unobserved"; + if (problem === "outside-range") + fixture.segments.push({ + index: 0, + type: "PT_LOAD", + offset: "0x200", + header_location: { offset: "0x0", bytes: "0x38" }, + file_backing: "file", + file_size: "0x1", + memory_size: "0x1", + virtual_address: "0x20000000000001", + physical_address: "0x0", + alignment: "0x1", + flags: "0x5", + }); + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(fixture)), + }); + expect( + await service.inspect({ path: "/artifacts/source-owned.core" }), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisOutputError", + capturedOutput: fixture.diagnostics, + }, + }); + }, +); diff --git a/src/application/binaryDiagnostics/RecordedCrashService.ts b/src/application/binaryDiagnostics/RecordedCrashService.ts new file mode 100644 index 0000000000..69307498a6 --- /dev/null +++ b/src/application/binaryDiagnostics/RecordedCrashService.ts @@ -0,0 +1,97 @@ +import { isAbsolute } from "node:path"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import type { RecordedCrashPort } from "./RecordedCrashPort.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { + inspectRecordedCrashInputSchema, + recordedCrashSchema, +} from "../../domain/native/recordedCrash.js"; +import { projectInputIssues } from "../../domain/inputIssueProjection.js"; +import { err, ok, type Result } from "../../domain/result.js"; + +const OPERATION = "inspect_recorded_crash"; + +/** Shared CLI/MCP validation and Evidence composition for recorded crash inspection. */ +export class RecordedCrashService { + constructor(readonly provider: RecordedCrashPort) {} + + /** Bind complete observations to the caller-selected artifact before creating Evidence. */ + async inspect( + rawInput: unknown, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION)); + const input = inspectRecordedCrashInputSchema.safeParse(rawInput); + if (!input.success) + return err( + new AnalysisInputError( + OPERATION, + { cause: input.error }, + projectInputIssues(input.error.issues, rawInput), + ), + ); + if (!isAbsolute(input.data.path)) + return err( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["path"], + reason: "invalid_format", + message: "Expected an absolute filesystem path on this host.", + }, + ]), + ); + const inspected = await this.provider.inspect(input.data, options); + if (!inspected.ok) return inspected; + const diagnostics = recordedCrashSchema.shape.diagnostics.safeParse( + inspected.value.diagnostics, + ); + const outputOptions = diagnostics.success + ? { capturedOutput: diagnostics.data } + : undefined; + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION, outputOptions)); + const report = recordedCrashSchema.safeParse(inspected.value); + if ( + !report.success || + report.data.artifact.path !== input.data.path || + (report.data.debugger.status === "available") !== + input.data.include_debugger_context + ) + return err( + new AnalysisOutputError( + OPERATION, + "Recorded crash provider returned malformed evidence or changed the selected artifact identity.", + outputOptions, + ), + ); + const value = report.data; + return ok( + createEvidence( + { + path: value.artifact.path, + format: "elf", + architecture: "x86_64", + sha256: value.artifact.sha256, + }, + this.provider.identity, + { + operation: OPERATION, + parameters: input.data, + result: value, + rawResult: value, + confidence: + value.debugger.status === "available" ? "derived" : "observed", + limitations: value.limitations, + locations: [{ kind: "artifact-path", path: value.artifact.path }], + }, + ), + ); + } +} diff --git a/src/application/evm/EvmInterfacePort.ts b/src/application/evm/EvmInterfacePort.ts new file mode 100644 index 0000000000..1d5c39ef45 --- /dev/null +++ b/src/application/evm/EvmInterfacePort.ts @@ -0,0 +1,15 @@ +import type { + AnalysisExecution, + ExecutionOptions, +} from "../AnalysisProvider.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { InspectEvmInterfaceInput } from "../../domain/evm/evmInterface.js"; +import type { Result } from "../../domain/result.js"; + +/** Replaceable offline interface-recovery engine, independent of wallets and chain clients. */ +export interface EvmInterfacePort { + inspect( + input: InspectEvmInterfaceInput, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/evm/EvmInterfaceService.test.ts b/src/application/evm/EvmInterfaceService.test.ts new file mode 100644 index 0000000000..9b01973884 --- /dev/null +++ b/src/application/evm/EvmInterfaceService.test.ts @@ -0,0 +1,124 @@ +import { expect, it } from "vitest"; +import { EvmInterfaceService } from "./EvmInterfaceService.js"; +import { + evmInterfaceExecution, + evmInterfaceFixture, +} from "../../../tests/fixtures/evm/interface.js"; +import { err, ok } from "../../domain/result.js"; +import { parseEvidence } from "../../domain/evidence.js"; +import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; + +it("returns full inline inferred Evidence and unmodified upstream representation", async () => { + const value = evmInterfaceFixture(); + const service = new EvmInterfaceService({ + inspect: () => Promise.resolve(ok(evmInterfaceExecution(value))), + }); + const result = await service.inspect({ + path: value.artifact.path, + encoding: "hex", + }); + if (!result.ok) throw result.error; + const evidence = parseEvidence(result.value); + expect(evidence.normalized_result).toEqual(value); + expect(evidence.raw_result).toEqual({ functions: [] }); + expect(evidence.confidence).toBe("inferred"); + expect(evidence.subject?.digest.sha256).toBe(value.artifact.sha256); +}); +it.each([ + { path: "relative.hex", encoding: "hex" }, + { path: "/selected", encoding: "auto" }, + { path: "/selected" }, + { path: "/selected", encoding: "raw", approval: true }, +])("rejects invalid selection before effects: %j", async (input) => { + const service = new EvmInterfaceService({ + inspect: () => { + throw new Error("must not start"); + }, + }); + expect(await service.inspect(input)).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); +}); +it.each(["carrier", "encoding", "digest", "no-subject"])( + "rejects mismatched producer binding: %s", + async (problem) => { + const value = evmInterfaceFixture(); + if (problem === "carrier") value.artifact.path = "/other"; + if (problem === "encoding") value.artifact.encoding = "raw"; + const observed = evmInterfaceExecution(value); + const execution = { + ...observed, + subject: + problem === "no-subject" + ? null + : { + path: "/artifacts/source-owned.hex", + sha256: + problem === "digest" ? "b".repeat(64) : value.artifact.sha256, + format: "file" as const, + }, + }; + const service = new EvmInterfaceService({ + inspect: () => Promise.resolve(ok(execution)), + }); + expect( + await service.inspect({ + path: "/artifacts/source-owned.hex", + encoding: "hex", + }), + ).toMatchObject({ + ok: false, + error: { _tag: "AnalysisOutputError", capturedOutput: value.diagnostics }, + }); + }, +); +it("preserves meaningful provider failure and cancellation before/after execution", async () => { + const failure = new AnalysisCapabilityUnavailableError( + "evm", + "inspect_evm_interface", + "Selected limiter missing", + ); + const service = new EvmInterfaceService({ + inspect: () => Promise.resolve(err(failure)), + }); + expect(await service.inspect({ path: "/code", encoding: "raw" })).toEqual( + err(failure), + ); + const controller = new AbortController(); + const observed = evmInterfaceFixture(); + observed.diagnostics = { + stdout: "selected\u0000", + stderr: "warning", + truncated: true, + }; + const cancelled = new EvmInterfaceService({ + inspect: () => { + controller.abort(); + return Promise.resolve(ok(evmInterfaceExecution(observed))); + }, + }); + expect( + await cancelled.inspect( + { path: "/artifacts/source-owned.hex", encoding: "hex" }, + { signal: controller.signal }, + ), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCancelledError", + capturedOutput: observed.diagnostics, + }, + }); + const untouched = new EvmInterfaceService({ + inspect: () => { + throw new Error("must not start"); + }, + }); + expect( + await untouched.inspect( + { path: "/code", encoding: "raw" }, + { signal: controller.signal }, + ), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); +}); diff --git a/src/application/evm/EvmInterfaceService.ts b/src/application/evm/EvmInterfaceService.ts new file mode 100644 index 0000000000..9769255c00 --- /dev/null +++ b/src/application/evm/EvmInterfaceService.ts @@ -0,0 +1,92 @@ +import { isAbsolute } from "node:path"; +import { z } from "zod"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import type { EvmInterfacePort } from "./EvmInterfacePort.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { + inspectEvmInterfaceInputSchema, + evmInterfaceSchema, +} from "../../domain/evm/evmInterface.js"; +import { projectInputIssues } from "../../domain/inputIssueProjection.js"; +import { err, ok, type Result } from "../../domain/result.js"; + +const OPERATION = "inspect_evm_interface"; +const diagnosticsSchema = z.object({ + diagnostics: evmInterfaceSchema.shape.diagnostics, +}); + +/** Shared carrier validation and typed inline Evidence for CLI/MCP. */ +export class EvmInterfaceService { + constructor(readonly provider: EvmInterfacePort) {} + + /** Preserve selected byte identity while clearly labeling recovered ABI candidates as inferences. */ + async inspect( + rawInput: unknown, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION)); + const input = inspectEvmInterfaceInputSchema.safeParse(rawInput); + if (!input.success) + return err( + new AnalysisInputError( + OPERATION, + { cause: input.error }, + projectInputIssues(input.error.issues, rawInput), + ), + ); + if (!isAbsolute(input.data.path)) + return err( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["path"], + reason: "invalid_format", + message: "Expected an absolute filesystem path on this host.", + }, + ]), + ); + const inspected = await this.provider.inspect(input.data, options); + if (!inspected.ok) return inspected; + const diagnostics = diagnosticsSchema.safeParse(inspected.value.result); + const outputOptions = diagnostics.success + ? { capturedOutput: diagnostics.data.diagnostics } + : undefined; + if (options?.signal?.aborted) + return err(new AnalysisCancelledError(OPERATION, outputOptions)); + const execution = inspected.value; + const report = evmInterfaceSchema.safeParse(execution.result); + if ( + !report.success || + report.data.artifact.path !== input.data.path || + report.data.artifact.encoding !== input.data.encoding || + execution.subject?.path !== input.data.path || + execution.subject.sha256 !== report.data.artifact.sha256 + ) + return err( + new AnalysisOutputError( + OPERATION, + "Interface adapter returned malformed data or changed the selected carrier identity/encoding.", + outputOptions, + ), + ); + return ok( + createEvidence(execution.subject, execution.provider, { + operation: OPERATION, + parameters: input.data, + result: report.data, + ...(execution.rawResult === null + ? {} + : { rawResult: execution.rawResult }), + confidence: "inferred", + locations: execution.locations, + limitations: execution.limitations, + }), + ); + } +} diff --git a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts index 1dac1b9a1d..a199f56268 100644 --- a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts +++ b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts @@ -43,7 +43,6 @@ const input = ( overrides: Partial = {}, ): RecordUnknownInput => recordUnknownInputSchema.parse({ - approved: true, question, severity: "high", domain: "protocol", @@ -60,7 +59,6 @@ const update = ( overrides: Readonly> = {}, ) => updateUnknownInputSchema.parse({ - approved: true, unknown_id: unknown.unknown_id, expected_revision: unknown.revision, status: "investigating", @@ -77,6 +75,78 @@ const update = ( }); describe("residual unknown registry reads and identity", () => { + it("merges record-only bundles without changing retained unknown history", () => { + const store = ledger(); + const created = store.recordUnknown( + input("Does a later record preserve this unknown?"), + mutation("record-only-import"), + ); + expect(created.ok).toBe(true); + const originalUnknowns = store.export().unknowns; + const addition = evidence("record-only-addition"); + const bundle = createEvidenceBundle([addition]); + + expect(store.import(bundle)).toEqual({ + ok: true, + value: { + recordsAdded: 1, + unknownsAdded: 0, + changed: true, + metadataChanged: false, + }, + }); + expect(store.export().unknowns).toEqual(originalUnknowns); + expect(store.import(bundle)).toEqual({ + ok: true, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: false, + metadataChanged: false, + }, + }); + }); +}); + +describe("residual unknown dependency histories", () => { + it("imports a valid dependency chain deeper than the JavaScript call stack", () => { + const records = []; + const unknowns: ResidualUnknown[] = []; + let previousId: string | undefined; + for (let index = 0; index < 10_000; index += 1) { + const mutationEvidence = mutation(`deep-chain-${index}`); + records.push(mutationEvidence); + const relationships = + previousId === undefined + ? [] + : [{ type: "depends-on" as const, unknown_id: previousId }]; + const unknown = createResidualUnknown( + input(`Does chain item ${index} meet the expected state?`, { + domain: "chain-profile", + required_authority: null, + relationships, + }), + mutationEvidence.evidence_id, + null, + ); + unknowns.push(unknown); + previousId = unknown.unknown_id; + } + const bundle = createEvidenceBundle(records, unknowns); + const store = ledger(); + + expect(store.import(bundle)).toEqual({ + ok: true, + value: { + recordsAdded: 10_000, + unknownsAdded: 10_000, + changed: true, + metadataChanged: false, + }, + }); + expect(store.export()).toEqual(bundle); + }); + it("returns detached unknowns and evidence bundles from every read surface", () => { const store = ledger(); expect(store.record(evidence("detached-record")).ok).toBe(true); diff --git a/src/application/investigation/EvidenceLedger.test.ts b/src/application/investigation/EvidenceLedger.test.ts index 46fe6e68d2..2eeb94616d 100644 --- a/src/application/investigation/EvidenceLedger.test.ts +++ b/src/application/investigation/EvidenceLedger.test.ts @@ -38,9 +38,55 @@ describe("evidence ledger recording", () => { }); expect(ledger.import(createEvidenceBundle([evidence]))).toEqual({ ok: true, - value: { recordsAdded: 0, unknownsAdded: 0, changed: false }, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: false, + metadataChanged: false, + }, }); expect(ledger.export().records).toEqual([evidence]); + const relocatedBundleRecord = createEvidence( + { ...TARGET, path: "/relocated/fixture-renamed" }, + PROVIDER, + { operation: "health", parameters: {}, result: true }, + ); + expect( + ledger.import(createEvidenceBundle([relocatedBundleRecord])), + ).toEqual({ + ok: true, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: true, + metadataChanged: true, + }, + }); + expect(ledger.get(evidence.evidence_id)).toEqual(relocatedBundleRecord); + expect(ledger.export().records).toEqual([relocatedBundleRecord]); + expect( + ledger.import(createEvidenceBundle([relocatedBundleRecord])), + ).toEqual({ + ok: true, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: false, + metadataChanged: false, + }, + }); + const conflictingBundle = createEvidenceBundle([ + evidence, + relocatedBundleRecord, + ]); + expect(ledger.import(conflictingBundle).ok).toBe(false); + expect(ledger.get(evidence.evidence_id)).toEqual(relocatedBundleRecord); + ledger.clear(); + expect(ledger.export().records).toEqual([]); + }); + + it("ignores JSON object key order during imported identity checks", () => { + const ledger = new EvidenceLedger(); const ordered = createEvidence(TARGET, PROVIDER, { operation: "health", parameters: { alpha: 1, beta: 2 }, @@ -60,11 +106,77 @@ describe("evidence ledger recording", () => { ); expect(ledger.import(reorderedBundle)).toEqual({ ok: true, - value: { recordsAdded: 0, unknownsAdded: 0, changed: false }, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: false, + metadataChanged: false, + }, }); ledger.clear(); expect(ledger.export().records).toEqual([]); }); +}); + +describe("evidence ledger metadata imports", () => { + it("reports and commits metadata replacement when importing unknown history", () => { + const ledger = new EvidenceLedger(); + const mutation = createEvidence(TARGET, PROVIDER, { + predicateType: "rea.residual-unknown-mutation", + operation: "record_unknown", + parameters: {}, + result: { action: "record", question: "Which path is current?" }, + }); + const created = ledger.recordUnknown( + recordUnknownInputSchema.parse({ + question: "Which path is current?", + severity: "high", + domain: "fixture", + required_authority: null, + required_confidence: "derived", + required_environment: null, + recommended_probes: [], + relationships: [], + }), + mutation, + ); + expect(created.ok).toBe(true); + const relocatedMutation = createEvidence( + { ...TARGET, path: "/relocated/mutation" }, + PROVIDER, + { + predicateType: "rea.residual-unknown-mutation", + operation: "record_unknown", + parameters: {}, + result: { action: "record", question: "Which path is current?" }, + }, + ); + expect(relocatedMutation.evidence_id).toBe(mutation.evidence_id); + const bundle = createEvidenceBundle( + [relocatedMutation], + ledger.export().unknowns, + ); + + expect(ledger.import(bundle)).toEqual({ + ok: true, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: true, + metadataChanged: true, + }, + }); + expect(ledger.get(mutation.evidence_id)).toEqual(relocatedMutation); + expect(ledger.import(bundle)).toEqual({ + ok: true, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: false, + metadataChanged: false, + }, + }); + }); it("retains more than ten thousand inline evidence records", () => { const ledger = new EvidenceLedger(); @@ -128,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", diff --git a/src/application/investigation/EvidenceLedger.ts b/src/application/investigation/EvidenceLedger.ts index 3b87c93d45..60fcc0ebd4 100644 --- a/src/application/investigation/EvidenceLedger.ts +++ b/src/application/investigation/EvidenceLedger.ts @@ -2,6 +2,7 @@ import canonicalize from "canonicalize"; import { createEvidenceBundle, + describeEvidenceBundleFailure, parseEvidenceBundle, validateResidualUnknownAddition, type EvidenceBundle, @@ -27,6 +28,7 @@ export interface EvidenceImportDelta { readonly recordsAdded: number; readonly unknownsAdded: number; readonly changed: boolean; + readonly metadataChanged: boolean; } type ImportResult = Result; @@ -67,14 +69,54 @@ export class EvidenceLedger { return err( new EvidenceIntegrityError("Evidence bundle validation failed", { cause, + userMessage: describeEvidenceBundleFailure(input, cause), }), ); } + // A record-only bundle cannot change the already-valid unknown graph. The + // bundle parser has validated every incoming record and its manifests, so + // checking ID conflicts and committing only the additions preserves the + // atomic merge without rebuilding and reparsing the entire ledger. + if (bundle.unknowns.length === 0) { + const additions = new Map(); + const replacements = new Map(); + let metadataChanged = false; + for (const evidence of bundle.records) { + const existing = this.#records.get(evidence.evidence_id); + if (existing !== undefined) { + if (!recordsAgree(existing, evidence)) + return err( + new EvidenceIntegrityError("Conflicting evidence record"), + ); + if (canonicalize(existing) !== canonicalize(evidence)) { + replacements.set(evidence.evidence_id, evidence); + metadataChanged = true; + } + continue; + } + additions.set(evidence.evidence_id, evidence); + } + for (const [id, evidence] of replacements) + this.#records.set(id, evidence); + for (const [id, evidence] of additions) this.#records.set(id, evidence); + return ok({ + recordsAdded: additions.size, + unknownsAdded: 0, + changed: additions.size > 0 || metadataChanged, + metadataChanged, + }); + } const pending = new Map(this.#records); + let metadataChanged = false; for (const evidence of bundle.records) { const existing = pending.get(evidence.evidence_id); if (existing !== undefined && !recordsAgree(existing, evidence)) return err(new EvidenceIntegrityError("Conflicting evidence record")); + if ( + existing !== undefined && + canonicalize(existing) !== canonicalize(evidence) + ) + metadataChanged = true; pending.set(evidence.evidence_id, evidence); } const pendingUnknowns = new Map(this.#unknownRevisions); @@ -108,7 +150,8 @@ export class EvidenceLedger { return ok({ recordsAdded: added, unknownsAdded, - changed: added > 0 || unknownsAdded > 0, + changed: added > 0 || unknownsAdded > 0 || metadataChanged, + metadataChanged, }); } diff --git a/src/application/investigation/InvestigationRecords.test.ts b/src/application/investigation/InvestigationRecords.test.ts index 6450590ec4..5ecf51e608 100644 --- a/src/application/investigation/InvestigationRecords.test.ts +++ b/src/application/investigation/InvestigationRecords.test.ts @@ -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({ @@ -133,11 +142,21 @@ describe("investigation record ownership", () => { ).toBe(true); expect(records.mergeEvidenceBundle(bundle)).toEqual({ ok: true, - value: { recordsAdded: 0, unknownsAdded: 1, changed: true }, + value: { + recordsAdded: 0, + unknownsAdded: 1, + changed: true, + metadataChanged: false, + }, }); expect(records.mergeEvidenceBundle(bundle)).toEqual({ ok: true, - value: { recordsAdded: 0, unknownsAdded: 0, changed: false }, + value: { + recordsAdded: 0, + unknownsAdded: 0, + changed: false, + metadataChanged: false, + }, }); records.clear(); expect(records.exportEvidenceBundle()).toEqual(createEvidenceBundle([])); diff --git a/src/application/javascript/ElectronBoundaryAnalysis.ts b/src/application/javascript/ElectronBoundaryAnalysis.ts index 99de494e2d..c1f8423175 100644 --- a/src/application/javascript/ElectronBoundaryAnalysis.ts +++ b/src/application/javascript/ElectronBoundaryAnalysis.ts @@ -1,7 +1,7 @@ import { compareCodePoints } from "../../domain/canonicalOrdering.js"; import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js"; -import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; +import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; @@ -27,7 +27,7 @@ export interface ElectronIpcPairingState { /** Flatten all per-file IPC facts into deterministic application records. */ export const collectElectronIpcRecords = ( - analysis: JavaScriptArtifactAnalysis, + analysis: JavaScriptModuleArtifactAnalysis, ): readonly ElectronIpcRecord[] => analysis.files .flatMap(({ file, javascript }) => @@ -79,7 +79,7 @@ export const unambiguousElectronIpcPairings = ( /** Summarize static Electron findings without treating gaps as absence. */ export const summarizeElectronBoundaries = ( - analysis: JavaScriptArtifactAnalysis, + analysis: JavaScriptModuleArtifactAnalysis, ): ElectronBoundarySummary => { const files = new Map(analysis.files.map(({ file }) => [file.path, file])); const javascript = analysis.files.flatMap(({ javascript: value }) => diff --git a/src/application/javascript/JavaScriptApplicationEmptyLiterals.test.ts b/src/application/javascript/JavaScriptApplicationEmptyLiterals.test.ts index a3a425bf99..1503bf634d 100644 --- a/src/application/javascript/JavaScriptApplicationEmptyLiterals.test.ts +++ b/src/application/javascript/JavaScriptApplicationEmptyLiterals.test.ts @@ -55,6 +55,24 @@ it.each([ await expect(analyzeSource(source)).resolves.toBeDefined(); }); +it("analyzes a plain long-line loader without package metadata", async () => { + const output = await analyzeSource( + `var Module = {}; fetch(""); new Worker(""); var payload = "${"x".repeat(65_536)}";`, + { "game-data.js": "Module.data = { ready: true };" }, + ); + const nodes = output.graph.nodes.filter( + ({ kind }) => kind === "endpoint" || kind === "worker", + ); + expect(nodes.map(({ kind }) => kind).sort()).toEqual(["endpoint", "worker"]); + for (const node of nodes) { + expect(node.identity).toMatchObject({ + strategy: "artifact-local-key", + key: "", + }); + expect(node.observations.map(({ label }) => label)).toEqual([null]); + } +}); + it("keeps exact values for empty literals without synthetic application labels", async () => { const output = await analyzeSource( "fetch(''); fetch('\"\"'); fetch('/api'); process.on('', () => 1);", diff --git a/src/application/javascript/JavaScriptApplicationEvidence.ts b/src/application/javascript/JavaScriptApplicationEvidence.ts index cb33dc58e0..2d26fb7ad5 100644 --- a/src/application/javascript/JavaScriptApplicationEvidence.ts +++ b/src/application/javascript/JavaScriptApplicationEvidence.ts @@ -7,7 +7,6 @@ import type { AnalyzeJavaScriptApplicationInput, JavaScriptApplicationAnalysisResult, } from "../../domain/javascript/javascriptApplicationAnalysis.js"; -import { jsonValueSchema } from "../../domain/jsonValue.js"; import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js"; /** Create Evidence for one deterministic local JavaScript application graph. */ @@ -26,7 +25,7 @@ export const createJavaScriptApplicationEvidence = ( predicateType: "rea.javascript-application-analysis", operation: "analyze_javascript_application", parameters: parameters(input), - result: jsonValueSchema.parse(result), + result, rawResult: null, confidence: "derived", authority: "shipped-artifact", diff --git a/src/application/javascript/JavaScriptApplicationService.test.ts b/src/application/javascript/JavaScriptApplicationService.test.ts index 8529ff3338..94b47f7e1d 100644 --- a/src/application/javascript/JavaScriptApplicationService.test.ts +++ b/src/application/javascript/JavaScriptApplicationService.test.ts @@ -8,7 +8,11 @@ import { z } from "zod"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; -import { compareApplicationVersionsEvidence } from "./JavaScriptApplicationWorkflowService.js"; +import { + compareApplicationVersionsEvidence, + compareJavaScriptExportShapesEvidence, + traceApplicationFeatureEvidence, +} from "./JavaScriptApplicationWorkflowService.js"; describe("JavaScript application failure diagnostics", () => { it("identifies the rejected result field in caller-visible diagnostics", async () => { @@ -49,6 +53,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" }, { @@ -74,6 +80,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 () => { @@ -121,10 +129,41 @@ 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); + }, + ); }); const CONTRADICTION_LIMITATION = - "Observed bytes of 1 artifact file(s) contradict declared integrity and are untrusted; contradicted nested archives were not expanded: addon.node."; + "Observed bytes of 1 artifact file(s) contradict declared integrity and are untrusted: addon.node."; describe("JavaScript application artifact integrity", () => { it("analyzes a signed-after-packaging native module only when the mismatch is recorded", async () => { @@ -135,7 +174,10 @@ describe("JavaScript application artifact integrity", () => { join(source, "package.json"), '{"name":"signed","main":"main.js"}', ); - await writeFile(join(source, "main.js"), "require('./addon.node');\n"); + await writeFile( + join(source, "main.js"), + "require('./addon.node'); export function value() { return {status: 'ready'}; }\n", + ); await writeFile(join(source, "addon.node"), "packed native bytes"); const archive = join(root, "app.asar"); await createPackageWithOptions(source, archive, { unpack: "*.node" }); @@ -194,6 +236,32 @@ describe("JavaScript application artifact integrity", () => { if (!comparison.ok) throw new Error("Expected comparison"); expect(comparison.value.normalized_result).toMatchObject({ coverage: { status: "partial", right_graph_status: "partial" }, + limitations: expect.arrayContaining([CONTRADICTION_LIMITATION]), + }); + const shapes = compareJavaScriptExportShapesEvidence({ + left: clean.value, + right: recorded.value, + left_module_path: "main.js", + left_export_name: "value", + right_module_path: "main.js", + right_export_name: "value", + }); + expect(shapes.ok).toBe(true); + if (!shapes.ok) throw shapes.error; + expect(shapes.value.normalized_result).toMatchObject({ + coverage: { status: "partial" }, + limitations: expect.arrayContaining([CONTRADICTION_LIMITATION]), + }); + const trace = traceApplicationFeatureEvidence({ + application: recorded.value, + seed: { kind: "string", value: "nonexistent-feature" }, + direction: "outgoing", + }); + expect(trace.ok).toBe(true); + if (!trace.ok) throw trace.error; + expect(trace.value.normalized_result).toMatchObject({ + graph: null, + limitations: expect.arrayContaining([CONTRADICTION_LIMITATION]), }); }); }); diff --git a/src/application/javascript/JavaScriptApplicationService.ts b/src/application/javascript/JavaScriptApplicationService.ts index b1a1bf6aac..59d07ef218 100644 --- a/src/application/javascript/JavaScriptApplicationService.ts +++ b/src/application/javascript/JavaScriptApplicationService.ts @@ -58,13 +58,27 @@ export const analyzeJavaScriptApplicationValidated = async ( integrity_policy: input.integrity_policy, }, options.signal, + options.progress, ); const { electron_summary: summary, ...application } = reconstructed; + await options.progress?.report({ + phase: "validate_javascript_application_result", + completed: 0, + total: 1, + message: "Validating the application analysis result", + }); const result = javascriptApplicationAnalysisResultSchema.parse({ ...application, summary, limitations: reconstructed.graph.limitations, }); + await options.progress?.report({ + phase: "create_javascript_application_evidence", + completed: 0, + total: 1, + message: "Creating and hashing application analysis Evidence", + }); + const evidence = createJavaScriptApplicationEvidence(input, result); await options.progress?.report({ phase: "analyze_javascript_application", completed: 1, @@ -72,7 +86,7 @@ export const analyzeJavaScriptApplicationValidated = async ( message: "Application graph and Electron boundaries reconstructed", terminal: true, }); - return ok(createJavaScriptApplicationEvidence(input, result)); + return ok(evidence); } catch (cause: unknown) { if (cause instanceof ArtifactReaderFailure) return err( diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts index 9422eacff8..aba362b768 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts @@ -1,7 +1,9 @@ import { createHash } from "node:crypto"; +import { setImmediate } from "node:timers/promises"; import { analyzeParsedJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; import { analyzeParsedJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; +import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js"; import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js"; import { flattenSourceMapLeaves } from "../../domain/sourceMapEnvelope.js"; @@ -18,6 +20,8 @@ import type { import type { AnalyzedJavaScriptArtifactFile, JavaScriptArtifactAnalysis, + JavaScriptModuleArtifactAnalysis, + JavaScriptModuleSemanticIr, JavaScriptHtmlScriptObservation, JavaScriptJsonModuleObservation, JavaScriptPackageObservation, @@ -25,9 +29,12 @@ import type { JavaScriptSourceMapOriginal, } from "./JavaScriptArtifactAnalysisTypes.js"; import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js"; +import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js"; -interface MutableArtifactAnalysis { - readonly files: AnalyzedJavaScriptArtifactFile[]; +interface MutableArtifactAnalysis< + SemanticIr extends JavaScriptModuleSemanticIr, +> { + readonly files: AnalyzedJavaScriptArtifactFile[]; readonly packages: JavaScriptPackageObservation[]; readonly jsonModules: JavaScriptJsonModuleObservation[]; readonly htmlScripts: JavaScriptHtmlScriptObservation[]; @@ -39,23 +46,88 @@ interface MutableArtifactAnalysis { truncatedScopes: number; } -interface ArtifactAnalysisContext { - readonly state: MutableArtifactAnalysis; +interface ArtifactAnalysisContext< + SemanticIr extends JavaScriptModuleSemanticIr, +> { + readonly state: MutableArtifactAnalysis; + readonly projectSemantics: ( + file: JavaScriptArtifactFile, + ir: JavaScriptSemanticIr, + ) => SemanticIr; } /** Analyze every text source from the artifact without a result quota. */ export const analyzeJavaScriptArtifactFiles = ( fileSet: JavaScriptArtifactFileSet, -): JavaScriptArtifactAnalysis => { - const state = emptyArtifactAnalysis(); - const context = { state }; +): JavaScriptArtifactAnalysis => + analyzeArtifactFiles(fileSet, (_file, ir) => ir); + +/** Project each file's full IR immediately, retaining only later module facts. */ +export const analyzeAndProjectJavaScriptArtifactFiles = async ( + fileSet: JavaScriptArtifactFileSet, + projectSemantics: ( + file: JavaScriptArtifactFile, + ir: JavaScriptSemanticIr, + ) => void, + beforeFile?: ( + file: JavaScriptArtifactFile, + completed: number, + total: number, + ) => Promise, +): Promise => { + const state = emptyArtifactAnalysis(); + const projectFileSemantics = ( + file: JavaScriptArtifactFile, + ir: JavaScriptSemanticIr, + ): JavaScriptModuleSemanticIr => { + projectSemantics(file, ir); + const programScopes = ir.scopes.filter(({ kind }) => kind === "program"); + const programScopeId = programScopes[0]?.scopeId; + const localNames = new Set( + ir.moduleLinks.map(({ localName }) => localName), + ); + const callableIds = new Set( + ir.moduleLinks.map(({ callableId }) => callableId), + ); + return { + scopes: programScopes, + bindings: ir.bindings.filter( + ({ name, scopeId }) => + scopeId === programScopeId && localNames.has(name), + ), + callables: ir.callables.filter(({ callableId }) => + callableIds.has(callableId), + ), + moduleLinks: ir.moduleLinks, + coverage: ir.coverage, + limitations: ir.limitations, + }; + }; + const context = { state, projectSemantics: projectFileSemantics }; + for (const [index, file] of fileSet.files.entries()) { + // Let progress, cancellation and garbage collection run between files. + await setImmediate(); + await beforeFile?.(file, index, fileSet.files.length); + analyzeArtifactFile(file, context); + } + return finalizeArtifactAnalysis(state); +}; + +const analyzeArtifactFiles = ( + fileSet: JavaScriptArtifactFileSet, + projectSemantics: ArtifactAnalysisContext["projectSemantics"], +): JavaScriptArtifactAnalysis => { + const state = emptyArtifactAnalysis(); + const context = { state, projectSemantics }; for (const file of fileSet.files) analyzeArtifactFile(file, context); return finalizeArtifactAnalysis(state); }; -const finalizeArtifactAnalysis = ( - state: MutableArtifactAnalysis, -): JavaScriptArtifactAnalysis => { +const finalizeArtifactAnalysis = < + SemanticIr extends JavaScriptModuleSemanticIr, +>( + state: MutableArtifactAnalysis, +): JavaScriptArtifactAnalysis => { const truncatedScopes = state.truncatedScopes; return { files: state.files, @@ -78,9 +150,9 @@ const finalizeArtifactAnalysis = ( }; }; -const analyzeArtifactFile = ( +const analyzeArtifactFile = ( file: JavaScriptArtifactFile, - context: ArtifactAnalysisContext, + context: ArtifactAnalysisContext, ): void => { const { state } = context; addStructuredObservations(file, state); @@ -107,7 +179,10 @@ const analyzeArtifactFile = ( state.files.push({ file, javascript: analysis, - semantic: semantics === null ? null : { ir: semantics }, + semantic: + semantics === null + ? null + : { ir: context.projectSemantics(file, semantics) }, }); state.visitedNodes += analysis.visited_ast_nodes; state.findings += staticFindings + (semantics?.moduleLinks.length ?? 0); @@ -120,13 +195,15 @@ const analyzeArtifactFile = ( const addSourceMap = ( file: JavaScriptArtifactFile, - context: ArtifactAnalysisContext, + context: ArtifactAnalysisContext, ): void => { const sourceMap = parseSourceMap(file); context.state.sourceMaps.push(sourceMap); }; -const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({ +const emptyArtifactAnalysis = < + SemanticIr extends JavaScriptModuleSemanticIr, +>(): MutableArtifactAnalysis => ({ files: [], packages: [], jsonModules: [], @@ -141,7 +218,7 @@ const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({ const addStructuredObservations = ( file: JavaScriptArtifactFile, - state: MutableArtifactAnalysis, + state: MutableArtifactAnalysis, ): void => { if (file.kind === "package-json") state.packages.push(parsePackage(file)); if (file.kind !== "json") return; @@ -205,26 +282,13 @@ const parseHtmlScripts = ( path: string, text: string, ): JavaScriptHtmlScriptObservation[] => { - const scripts: JavaScriptHtmlScriptObservation[] = []; - const baseHref = htmlBaseHref(text); - const pattern = /]*\bsrc\s*=\s*(["'])([^"']+)\1[^>]*>/giu; - for (const match of text.matchAll(pattern)) { - const script = match[2]; - if (script === undefined) continue; - const start = match.index; - scripts.push({ - html_path: path, - script_path: script, - base_href: baseHref, - location: rangeForOffsets(text, start, start + match[0].length), - }); - } - return scripts; -}; - -const htmlBaseHref = (text: string): string | null => { - const match = /]*\bhref\s*=\s*(["'])([^"']+)\1[^>]*>/iu.exec(text); - return match?.[2] ?? null; + const { scripts, baseHref } = htmlArtifactReferences(text); + return scripts.map(({ scriptPath, startOffset, endOffset }) => ({ + html_path: path, + script_path: scriptPath, + base_href: baseHref, + location: rangeForOffsets(text, startOffset, endOffset), + })); }; const parseSourceMap = ( diff --git a/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts b/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts index d1cdd2a6b5..152b3520af 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts @@ -87,18 +87,33 @@ export type JavaScriptJsonModuleObservation = JavaScriptStructuredObservation & } ); +/** Module facts needed after file-local semantic projection has finished. */ +export type JavaScriptModuleSemanticIr = Pick< + JavaScriptSemanticIr, + | "scopes" + | "bindings" + | "callables" + | "moduleLinks" + | "coverage" + | "limitations" +>; + /** One relevant file plus optional AST-only JavaScript facts. */ -export interface AnalyzedJavaScriptArtifactFile { +export interface AnalyzedJavaScriptArtifactFile< + SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr, +> { readonly file: JavaScriptArtifactFile; readonly javascript: JavaScriptStaticAnalysis | null; readonly semantic: { - readonly ir: JavaScriptSemanticIr; + readonly ir: SemanticIr; } | null; } /** Complete static-analysis projection before graph construction. */ -export interface JavaScriptArtifactAnalysis { - readonly files: readonly AnalyzedJavaScriptArtifactFile[]; +export interface JavaScriptArtifactAnalysis< + SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr, +> { + readonly files: readonly AnalyzedJavaScriptArtifactFile[]; readonly packages: readonly JavaScriptPackageObservation[]; readonly json_modules: readonly JavaScriptJsonModuleObservation[]; readonly html_scripts: readonly JavaScriptHtmlScriptObservation[]; @@ -110,3 +125,7 @@ export interface JavaScriptArtifactAnalysis { readonly truncated_scopes: number; readonly limitations: readonly string[]; } + +/** Artifact facts retaining only semantics used by application relationships. */ +export type JavaScriptModuleArtifactAnalysis = + JavaScriptArtifactAnalysis; diff --git a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts index 69a43ab905..1a05db66e2 100644 --- a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts +++ b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts @@ -1,9 +1,9 @@ -import type { ArtifactInventorySnapshot } from "../ArtifactInventory.js"; +import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js"; import { createJavaScriptApplicationGraph, type JavaScriptApplicationGraph, } from "../../domain/javascript/javascriptApplicationGraph.js"; -import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; +import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFileSet } from "../../domain/javascript/javascriptArtifactFiles.js"; import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js"; import type { JavaScriptArtifactGraphContext } from "./JavaScriptArtifactGraphContext.js"; @@ -37,7 +37,7 @@ import { export const buildJavaScriptArtifactGraph = ( snapshot: ArtifactInventorySnapshot, fileSet: JavaScriptArtifactFileSet, - analysis: JavaScriptArtifactAnalysis, + analysis: JavaScriptModuleArtifactAnalysis, ): JavaScriptApplicationGraph => { const accumulator = new JavaScriptArtifactGraphAccumulator(); const root = createJavaScriptArtifactRootNode(accumulator, snapshot); @@ -115,7 +115,7 @@ export const integrityContradictionLimitations = ( snapshot.integrity_contradictions.length === 0 ? [] : [ - `Observed bytes of ${String(snapshot.integrity_contradictions.length)} artifact file(s) contradict declared integrity and are untrusted; contradicted nested archives were not expanded: ${snapshot.integrity_contradictions.map(({ logical_path: path }) => path).join(", ")}.`, + `Observed bytes of ${String(snapshot.integrity_contradictions.length)} artifact file(s) contradict declared integrity and are untrusted: ${snapshot.integrity_contradictions.map(({ logical_path: path }) => path).join(", ")}.`, ]; const graphLimitations = ( diff --git a/src/application/javascript/JavaScriptArtifactGraphContext.ts b/src/application/javascript/JavaScriptArtifactGraphContext.ts index 8e398bc77a..0ef275eeb9 100644 --- a/src/application/javascript/JavaScriptArtifactGraphContext.ts +++ b/src/application/javascript/JavaScriptArtifactGraphContext.ts @@ -1,4 +1,4 @@ -import type { ArtifactInventorySnapshot } from "../ArtifactInventory.js"; +import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js"; import { completeApplicationCoverage, partialApplicationCoverage, @@ -6,7 +6,7 @@ import { } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; +import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFile, JavaScriptArtifactFileSet, @@ -29,7 +29,7 @@ export interface JavaScriptArtifactGraphContext { readonly accumulator: JavaScriptArtifactGraphAccumulator; readonly snapshot: ArtifactInventorySnapshot; readonly fileSet: JavaScriptArtifactFileSet; - readonly analysis: JavaScriptArtifactAnalysis; + readonly analysis: JavaScriptModuleArtifactAnalysis; readonly root: ApplicationNode; readonly filesByPath: ReadonlyMap; readonly fileNodes: Map; @@ -209,7 +209,7 @@ export const addUnavailableStaticParseScope = ( /** Coverage for one bounded JavaScript AST analysis. */ export const javascriptAnalysisCoverage = ( analysis: NonNullable< - JavaScriptArtifactAnalysis["files"][number]["javascript"] + JavaScriptModuleArtifactAnalysis["files"][number]["javascript"] >, ): JavaScriptArtifactGraphCoverage => analysis.parse_status === "complete" diff --git a/src/application/javascript/JavaScriptArtifactGraphDocuments.ts b/src/application/javascript/JavaScriptArtifactGraphDocuments.ts index 18258ef131..f17293f5e1 100644 --- a/src/application/javascript/JavaScriptArtifactGraphDocuments.ts +++ b/src/application/javascript/JavaScriptArtifactGraphDocuments.ts @@ -5,7 +5,10 @@ import { linkElectronRoleToAsset, type JavaScriptArtifactGraphContext, } from "./JavaScriptArtifactGraphContext.js"; -import { artifactObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js"; +import { + artifactObservationEvidence, + staticInferenceEvidence, +} from "./JavaScriptArtifactGraphEvidence.js"; import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; @@ -57,7 +60,32 @@ export const addJavaScriptHtmlRoles = ( htmlBaseHref: script.base_href, }); const resolvedPath = resolution.resolved_path; - if (resolvedPath === null) continue; + if (resolvedPath === null) { + context.accumulator.addNode({ + kind: role.kind, + identity: role.identity, + observations: [ + { + label: script.script_path, + properties: { + mechanism: "html-script-reference", + script_path: script.script_path, + base_href: script.base_href, + ...resolution, + }, + evidence: staticInferenceEvidence({ + sha256: html.sha256, + path: html.path, + range: script.location, + operation: "resolve-html-script-reference", + coverage: completeApplicationCoverage(), + limitations: resolution.limitations, + }), + }, + ], + }); + continue; + } const resolved = context.fileNodes.get(resolvedPath); if (resolved === undefined) continue; addStaticInferenceEdge(context, { diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.ts index 7a135fdead..1299f28dc0 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.ts @@ -1,7 +1,7 @@ -import type { ArtifactInventorySnapshot } from "../ArtifactInventory.js"; +import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js"; import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; -import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; +import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactContainer, JavaScriptArtifactFile, @@ -270,7 +270,7 @@ export const addJavaScriptPackageNodes = ( const createFileTarget = ( context: JavaScriptArtifactGraphContext, file: JavaScriptArtifactFile, - javascript: JavaScriptArtifactAnalysis["files"][number]["javascript"], + javascript: JavaScriptModuleArtifactAnalysis["files"][number]["javascript"], ): ApplicationNode => { const kind = artifactFileNodeKind(file.kind); const json = context.analysis.json_modules.find( diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts index 5223926e83..f798ebfc3e 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts @@ -9,6 +9,42 @@ import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js"; import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; +describe("artifact URI scheme classification", () => { + it.each(["html-reference", "module-specifier", "url-reference"] as const)( + "keeps digit-bearing schemes external in %s even when a local file matches", + (context) => { + const files = fileMap([ + file("index.html", "root"), + file("web3:app.js", "root"), + ]); + expect( + resolve({ + declaredPath: "web3:app.js", + sourcePath: "index.html", + context, + files, + }), + ).toMatchObject({ resolution_status: "external", resolved_path: null }); + }, + ); + + it("keeps references under a digit-bearing external base href external", () => { + const files = fileMap([ + file("index.html", "root"), + file("web3:assets/app.js", "root"), + ]); + expect( + resolve({ + declaredPath: "app.js", + sourcePath: "index.html", + context: "html-reference", + htmlBaseHref: "web3:assets/", + files, + }), + ).toMatchObject({ resolution_status: "external", resolved_path: null }); + }); +}); + describe("artifact-local graph identity", () => { it("preserves complete long namespace and key values", () => { const digest = "a".repeat(64); @@ -326,6 +362,35 @@ describe("contextual JavaScript package path resolution", () => { }); }); +describe("generic package entry heuristics", () => { + it.each([ + ['{"main":"actual.cjs","module":"bundler.mjs"}', "actual.cjs"], + ['{"module":"bundler.mjs"}', "bundler.mjs"], + ])( + "preserves generic entry heuristics without a module kind for %s", + (metadata, entry) => { + const files = fileMap([ + file("app/consumer.js", "root"), + file("app/node_modules/fixture/package.json", "root", metadata), + file("app/node_modules/fixture/actual.cjs", "root"), + file("app/node_modules/fixture/bundler.mjs", "root"), + file("app/node_modules/fixture/index.js", "root"), + ]); + expect( + resolve({ + declaredPath: "fixture", + sourcePath: "app/consumer.js", + context: "module-specifier", + files, + }), + ).toMatchObject({ + resolution_status: "resolved", + resolved_path: `app/node_modules/fixture/${entry}`, + }); + }, + ); +}); + describe("directory package entrypoint precedence", () => { it.each([ ["require", '{"main":"actual.cjs"}', "actual.cjs"], @@ -640,3 +705,52 @@ const file = ( ? { included: false, reason: "invalid-utf8" } : { included: true, value: text }, }); + +describe("exact HTML resource resolution", () => { + it.each([ + ["extension", ["app.js"]], + ["directory index", ["app/index.js"]], + ["directory package", ["app/package.json", "app/entry.js"]], + ] as const)("does not infer a file from %s fallback", (_name, paths) => { + const files = fileMap([ + file("index.html", "root"), + ...paths.map((path) => + file( + path, + "root", + path.endsWith("package.json") ? '{"main":"entry.js"}' : "", + ), + ), + ]); + expect( + resolve({ + declaredPath: "./app", + sourcePath: "index.html", + context: "html-reference", + files, + }), + ).toMatchObject({ resolution_status: "not-found", resolved_path: null }); + }); + + it("prefers an exact extensionless resource and retains URL suffix handling", () => { + const files = fileMap([ + file("renderer/index.html", "root"), + file("assets/app", "root"), + file("assets/app.js", "root"), + file("assets/app/index.js", "root"), + ]); + expect( + resolve({ + declaredPath: "./app?cache=1#v2", + sourcePath: "renderer/index.html", + context: "html-reference", + htmlBaseHref: "/assets/", + files, + }), + ).toMatchObject({ + declared_path: "./app?cache=1#v2", + resolution_status: "resolved", + resolved_path: "assets/app", + }); + }); +}); diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts index 3450ffd20b..dca7ac8e7e 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts @@ -86,13 +86,25 @@ const resolvePackagePath = ( input: ResolveArtifactPathInput, packageChain: ReadonlySet, mode: "full" | "files-and-index" = "full", + exportsTargetUrl = false, ): ArtifactPathResolution => { const rejected = rejectDeclaration(input); if (rejected !== null) return rejected; - const candidate = contextualCandidate(input); + const candidate = contextualCandidate(input, exportsTargetUrl); if (typeof candidate !== "string") return candidate; const confined = confineCandidate(input, candidate); if (typeof confined !== "string") return confined; + if (input.context === "html-reference") + return outcome( + input, + resolveFileCandidates(input, [confined]) ?? { + resolvedPath: null, + status: "not-found", + limitations: [ + `The exact HTML resource ${confined} was not found among the selected application files.`, + ], + }, + ); const resolved = mode === "files-and-index" ? (resolveFileCandidates(input, [ @@ -124,11 +136,13 @@ const rejectDeclaration = ( const contextualCandidate = ( input: ResolveArtifactPathInput, + exportsTargetUrl: boolean, ): string | ArtifactPathResolution => { const { context } = input; - const declared = + let declared = context === "html-reference" || context === "url-reference" || + exportsTargetUrl || (context === "module-specifier" && input.moduleKind !== "require") ? stripQueryAndFragment(input.declaredPath) : input.declaredPath; @@ -146,6 +160,24 @@ const contextualCandidate = ( return unresolvedOutcome(input, "external", [ "URL schemes and protocol-relative URLs are outside this local artifact path context.", ]); + // Exports targets use URL paths for both loaders; legacy main fields remain literal. + if ( + exportsTargetUrl || + (context === "module-specifier" && input.moduleKind !== "require") + ) { + try { + declared = decodeURIComponent(declared); + } catch (cause: unknown) { + void cause; + return unresolvedOutcome(input, "rejected", [ + "The module URL path contains malformed percent encoding.", + ]); + } + if (declared.includes("\0")) + return unresolvedOutcome(input, "rejected", [ + "The decoded module URL path contains NUL.", + ]); + } const relative = declared.startsWith("/") ? declared.slice(1) : declared; return declared.startsWith("/") ? relative @@ -317,6 +349,12 @@ const resolveCandidate = ( `Directory package metadata ${packagePath} is not valid package JSON.`, ], }; + if (main.status === "rejected") + return { + resolvedPath: null, + status: "rejected", + limitations: [`Package metadata ${packagePath}: ${main.limitation}`], + }; if (main.status === "unmatched") return { resolvedPath: null, @@ -353,7 +391,9 @@ const resolveCandidate = ( if (index !== null) return index; } // Preserve recursive artifact package lookup only after legacy file/index fallbacks. - return candidateOutcome(resolvePackagePath(entryInput, chain)); + return candidateOutcome( + resolvePackagePath(entryInput, chain, "full", main.source === "exports"), + ); }; const candidateOutcome = ( @@ -408,6 +448,7 @@ const packageEntry = ( } | { readonly status: "missing" } | { readonly status: "invalid" } + | { readonly status: "rejected"; readonly limitation: string } | { readonly status: "unmatched"; readonly declared: readonly string[] } => { try { const value: unknown = JSON.parse(text); @@ -420,10 +461,12 @@ const packageEntry = ( ? { ...exported, source: "exports" } : exported; } - const preferred = - moduleKind === "import" - ? [Reflect.get(value, "module"), Reflect.get(value, "main")] - : [Reflect.get(value, "main"), Reflect.get(value, "module")]; + // Node imports and requires ignore the bundler-only module field. Keep + // that fallback for callers without an explicit module-loading kind. + const preferred = [ + Reflect.get(value, "main"), + ...(moduleKind === undefined ? [Reflect.get(value, "module")] : []), + ]; const entry = preferred.find((candidate) => candidate !== undefined); if (entry === undefined) return { status: "missing" }; const legacy = packagePathValue(entry); @@ -439,26 +482,53 @@ const packageExport = ( value: unknown, moduleKind: ResolveArtifactPathInput["moduleKind"], ): PackageExportOutcome => { - if (typeof value === "string") return packagePathValue(value); - if (typeof value !== "object" || value === null || Array.isArray(value)) - return { status: "invalid" }; - const root = Reflect.get(value, ".") ?? value; - if (typeof root === "string") return packagePathValue(root); - if (typeof root !== "object" || root === null || Array.isArray(root)) - return { status: "invalid" }; + const root = + typeof value === "object" && value !== null && Object.hasOwn(value, ".") + ? Reflect.get(value, ".") + : value; const flattened = exportTargets(root, packageExportConditions(moduleKind)); - if (flattened.kind === "invalid") return { status: "invalid" }; + if (flattened.kind === "invalid") + return { status: "rejected", limitation: flattened.limitation }; const first = flattened.kind === "unmatched" ? undefined : flattened.values[0]; return first === undefined - ? { status: "unmatched", declared: Object.keys(root) } + ? { + status: "unmatched", + declared: + typeof root === "object" && root !== null ? Object.keys(root) : [], + } : { status: "value", value: first }; }; type ExportTargets = | { readonly kind: "targets"; readonly values: readonly string[] } | { readonly kind: "unmatched" } - | { readonly kind: "invalid" }; + | { readonly kind: "invalid"; readonly limitation: string }; + +const invalidExportTarget = ( + value: unknown, + reason: string, +): ExportTargets => ({ + kind: "invalid", + limitation: `The package exports target ${JSON.stringify(value) ?? String(value)} ${reason}.`, +}); + +// Node checks raw target segments before URL normalization. Compare encoded +// segment names here; full URL decoding follows array target selection. +const forbiddenExportSegment = (value: string): string | undefined => + value + .slice(2) + .split(/[\\/]/u) + .find((raw) => { + const segment = raw.replace(/%[0-9a-f]{2}/giu, (encoded) => + String.fromCharCode(Number.parseInt(encoded.slice(1), 16)), + ); + return ( + segment === "." || + segment === ".." || + segment.toLowerCase() === "node_modules" + ); + }); /** * Resolve a target in Node's declared order. Explicit null blocks an active @@ -470,26 +540,39 @@ const exportTargets = ( value: unknown, conditions: ReadonlySet, ): ExportTargets => { - if (typeof value === "string") - return value.startsWith("./") + if (typeof value === "string") { + if (!value.startsWith("./")) + return invalidExportTarget(value, 'must start with "./"'); + const forbidden = forbiddenExportSegment(value); + return forbidden === undefined ? { kind: "targets", values: [value] } - : { kind: "invalid" }; + : invalidExportTarget( + value, + `contains forbidden path segment ${JSON.stringify(forbidden)} before URL normalization`, + ); + } if (value === null) return { kind: "targets", values: [] }; if (Array.isArray(value)) { - let invalid = false; + let invalid: string | null = null; for (const entry of value) { const nested = exportTargets(entry, conditions); if (nested.kind === "invalid") { - invalid = true; + invalid = nested.limitation; continue; } if (nested.kind === "unmatched") continue; - invalid = false; + invalid = null; if (nested.values.length > 0) return nested; } - return invalid ? { kind: "invalid" } : { kind: "targets", values: [] }; + return invalid === null + ? { kind: "targets", values: [] } + : { kind: "invalid", limitation: invalid }; } - if (typeof value !== "object") return { kind: "invalid" }; + if (typeof value !== "object") + return invalidExportTarget( + value, + "must be a relative string, object, array, or null", + ); for (const [condition, target] of Object.entries(value)) { if (!conditions.has(condition)) continue; const nested = exportTargets(target, conditions); @@ -516,7 +599,7 @@ const packageExportConditions = ( type PackageExportOutcome = | { readonly status: "value"; readonly value: string } - | { readonly status: "invalid" } + | { readonly status: "rejected"; readonly limitation: string } | { readonly status: "unmatched"; readonly declared: readonly string[] }; const packagePathValue = ( diff --git a/src/application/javascript/JavaScriptArtifactReconstruction.ts b/src/application/javascript/JavaScriptArtifactReconstruction.ts index d700430f68..273ad665a3 100644 --- a/src/application/javascript/JavaScriptArtifactReconstruction.ts +++ b/src/application/javascript/JavaScriptArtifactReconstruction.ts @@ -6,7 +6,7 @@ import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascr import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js"; import type { IntegrityContradiction } from "../../domain/artifactGraph.js"; -import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js"; +import { analyzeAndProjectJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js"; import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js"; import { buildJavaScriptArtifactGraph, @@ -16,9 +16,10 @@ import { javascriptArtifactReconstructionInputSchema, type JavaScriptArtifactReconstructionInput, } from "./JavaScriptArtifactReconstructionInput.js"; -import { scanCanonicalArtifactInventory } from "../ArtifactInventory.js"; +import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/ArtifactInventory.js"; import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js"; -import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js"; +import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js"; +import type { ProgressReporter } from "../ProgressReporter.js"; /** Application-layer result retaining local diagnostics outside the canonical graph. */ export interface JavaScriptArtifactReconstructionResult { @@ -50,7 +51,11 @@ export interface JavaScriptArtifactReconstructionResult { export const reconstructJavaScriptArtifact = async ( rawInput: unknown, signal?: AbortSignal, + progress?: ProgressReporter, ): Promise => { + const reportPhase = async (phase: string, message: string): Promise => { + await progress?.report({ phase, completed: 0, total: 1, message }); + }; const input = javascriptArtifactReconstructionInputSchema.parse(rawInput); abortIfNeeded(signal); const path = await realpath(input.input_path); @@ -66,16 +71,46 @@ export const reconstructJavaScriptArtifact = async ( ); const reader = createReader(path, format); try { + await reportPhase( + "read_javascript_artifacts", + "Reading inventoried JavaScript application sources", + ); const files = await readJavaScriptArtifactFiles(reader, snapshot, signal); - const analysis = analyzeJavaScriptArtifactFiles(files); + await reportPhase( + "parse_javascript_sources", + `Parsing and projecting ${String(files.files.length)} application source files`, + ); + const semanticProjection = createJavaScriptSemanticGraphProjection(); + const analysis = await analyzeAndProjectJavaScriptArtifactFiles( + files, + semanticProjection.projectFile, + async (file, completed, total) => { + abortIfNeeded(signal); + await progress?.report({ + phase: "parse_javascript_source", + completed: 0, + total: 1, + message: `Parsing and projecting ${file.path} (${String(completed + 1)}/${String(total)})`, + }); + abortIfNeeded(signal); + }, + ); abortIfNeeded(signal); + await reportPhase( + "build_javascript_application_graph", + "Constructing application and Electron boundary relationships", + ); const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis); - const semanticGraph = buildJavaScriptSemanticGraph({ - rootArtifactSha256: snapshot.manifest.root_sha256, - applicationGraph: graph, + await reportPhase( + "build_javascript_semantic_graph", + "Binding and validating static semantic relationships", + ); + const semanticGraph = semanticProjection.finish( + snapshot.manifest.root_sha256, + graph, analysis, - sourceLimitations: integrityContradictionLimitations(snapshot), - }); + integrityContradictionLimitations(snapshot), + ); return { input_path: path, format, diff --git a/src/application/javascript/JavaScriptModuleRelationships.ts b/src/application/javascript/JavaScriptModuleRelationships.ts index d3b688ca4b..e87d165bdd 100644 --- a/src/application/javascript/JavaScriptModuleRelationships.ts +++ b/src/application/javascript/JavaScriptModuleRelationships.ts @@ -8,7 +8,6 @@ import { } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import type { JavaScriptModuleOrigin, - JavaScriptSemanticIr, JavaScriptSemanticModuleLink, } from "../../domain/javascript/javascriptSemanticIr.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; @@ -28,8 +27,10 @@ import { } from "./JavaScriptArtifactPathResolution.js"; import { projectJavaScriptExportReturnShapes } from "./JavaScriptReturnShapeProjection.js"; +import type { JavaScriptModuleSemanticIr } from "./JavaScriptArtifactAnalysisTypes.js"; + interface SemanticAnalysis { - readonly ir: JavaScriptSemanticIr; + readonly ir: JavaScriptModuleSemanticIr; } interface RelationshipInput { @@ -310,7 +311,7 @@ const unresolvedModuleNode = ( }); const moduleOriginForExport = ( - ir: JavaScriptSemanticIr, + ir: JavaScriptModuleSemanticIr, link: JavaScriptSemanticModuleLink, ): JavaScriptModuleOrigin | null => { if (link.specifier !== null) @@ -374,7 +375,7 @@ const semanticCoverage = ( const moduleFormat = ( path: string, - ir: JavaScriptSemanticIr, + ir: JavaScriptModuleSemanticIr, ): "commonjs" | "esm" | "mixed" | "unknown" => { const extension = posix.extname(path).toLowerCase(); if (extension === ".mjs") return "esm"; diff --git a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts index 63f44337eb..6d4ac24bec 100644 --- a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts +++ b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts @@ -1,7 +1,14 @@ import { describe, expect, it } from "vitest"; +import { execFile } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; +import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; +import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js"; const filesFor = (paths: readonly string[]) => new Map( @@ -20,37 +27,46 @@ const filesFor = (paths: readonly string[]) => ]), ); -const cases = ["#", "?"].flatMap((punctuation) => - (["require", "import", undefined] as const).flatMap((moduleKind) => - (["literal", "stripped", "both"] as const).map((layout) => ({ - punctuation, - moduleKind, - layout, - })), - ), -); - describe("module specifier punctuation", () => { - it.each(cases)( - "resolves $moduleKind with $punctuation and $layout files", - ({ punctuation, moduleKind, layout }) => { + it.each(["#", "?"])( + "keeps CommonJS punctuation and strips URL suffixes for ESM (%s)", + (punctuation) => { const literal = `app/dep.cjs${punctuation}literal.cjs`; const stripped = "app/dep.cjs"; - const files = filesFor([ - ...(layout !== "stripped" ? [literal] : []), - ...(layout !== "literal" ? [stripped] : []), - ]); - const expected = moduleKind === "require" ? literal : stripped; - const result = resolveArtifactPathByContext({ + const commonJs = resolveArtifactPathByContext({ declaredPath: `./dep.cjs${punctuation}literal.cjs`, sourcePath: "app/main.js", context: "module-specifier", - files, - ...(moduleKind === undefined ? {} : { moduleKind }), + files: filesFor([literal, stripped]), + moduleKind: "require", }); - expect(result).toMatchObject({ - resolution_status: files.has(expected) ? "resolved" : "not-found", - resolved_path: files.has(expected) ? expected : null, + expect(commonJs).toMatchObject({ + resolution_status: "resolved", + resolved_path: literal, + }); + + const esm = resolveArtifactPathByContext({ + declaredPath: `./dep.cjs${punctuation}literal.cjs`, + sourcePath: "app/main.js", + context: "module-specifier", + files: filesFor([literal, stripped]), + moduleKind: "import", + }); + expect(esm).toMatchObject({ + resolution_status: "resolved", + resolved_path: stripped, + }); + + const noStrippedTarget = resolveArtifactPathByContext({ + declaredPath: `./dep.cjs${punctuation}literal.cjs`, + sourcePath: "app/main.js", + context: "module-specifier", + files: filesFor([literal]), + moduleKind: "import", + }); + expect(noStrippedTarget).toMatchObject({ + resolution_status: "not-found", + resolved_path: null, }); }, ); @@ -75,3 +91,241 @@ describe("module specifier punctuation", () => { }, ); }); + +describe("ESM module URL decoding", () => { + it.each([ + ["./plain.mjs", "plain.mjs"], + ["./space%20name.mjs", "space name.mjs"], + ["./pr%C3%A9load.mjs", "préload.mjs"], + ["./name%3F%23.mjs", "name?#.mjs"], + ["./percent%2520.mjs?cache=1#v2", "percent%20.mjs"], + ])( + "points %s imports to the file loaded by Node", + async (specifier, target) => { + const root = await createTestTempDirectory("rea-module-url-"); + const main = join(root, "main.mjs"); + await writeFile( + main, + `import value from ${JSON.stringify(specifier)}; console.log(value);`, + ); + await writeFile( + join(root, target), + `export default ${JSON.stringify(target)};`, + ); + const encoded = specifier.slice(2).split("?", 1)[0]?.split("#", 1)[0]; + if (encoded !== undefined && encoded !== target) + await writeFile(join(root, encoded), 'export default "encoded decoy";'); + const native = await promisify(execFile)(process.execPath, [main], { + timeout: 5_000, + }); + expect(native.stdout.trim()).toBe(target); + + const result = await analyzeJavaScriptApplication({ + input_path: root, + format: "directory", + }); + expect(result.ok).toBe(true); + if (!result.ok) throw result.error; + const { graph } = javascriptApplicationAnalysisResultSchema.parse( + result.value.normalized_result, + ); + const imports = graph.edges.filter( + (edge) => + edge.relation === "imports" && + edge.properties.specifier === specifier, + ); + expect(imports.length).toBeGreaterThan(0); + for (const edge of imports) + expect(edge.properties.resolved_path).toBe(target); + }, + ); + + it("preserves CommonJS literal percent names", () => { + expect( + resolveArtifactPathByContext({ + declaredPath: "./space%20name.cjs", + sourcePath: "app/main.js", + context: "module-specifier", + moduleKind: "require", + files: filesFor(["app/space%20name.cjs", "app/space name.cjs"]), + }), + ).toMatchObject({ + resolved_path: "app/space%20name.cjs", + resolution_status: "resolved", + }); + }); + + it.each(["./invalid%.mjs", "./invalid%C3.mjs", "./nul%00.mjs"])( + "rejects malformed or NUL URL %s", + (declaredPath) => { + expect( + resolveArtifactPathByContext({ + declaredPath, + sourcePath: "app/main.js", + context: "module-specifier", + moduleKind: "import", + files: filesFor([`app/${declaredPath.slice(2)}`]), + }), + ).toMatchObject({ resolved_path: null, resolution_status: "rejected" }); + }, + ); +}); + +const packageEntryUrlCases = [ + ["exports", "./actual%20file.cjs", "actual file.cjs"], + ["exports", "./actual.cjs?variant", "actual.cjs"], + ["exports", "./actual.cjs#variant", "actual.cjs"], + ["exports", "./actual%23file.cjs", "actual#file.cjs"], + ["exports", "./actual%3Ffile.cjs", "actual?file.cjs"], + ["exports", "./actual%252Ffile.cjs", "actual%2Ffile.cjs"], + ["exports", "./actual.cjs", "actual.cjs"], + ["main", "./actual%20file.cjs", "actual%20file.cjs"], + ["main", "./actual?file.cjs", "actual?file.cjs"], + ["main", "./actual#file.cjs", "actual#file.cjs"], +] as const; + +describe("package exports URL paths", () => { + it.each( + (["import", "require"] as const).flatMap((moduleKind) => + packageEntryUrlCases.map(([field, declaredPath, target]) => ({ + moduleKind, + field, + declaredPath, + target, + })), + ), + )( + "matches Node $moduleKind for $field $declaredPath", + async ({ moduleKind, field, declaredPath, target }) => { + const root = await createTestTempDirectory("rea-package-export-url-"); + const packageRoot = join(root, "node_modules", "fixture"); + await mkdir(packageRoot, { recursive: true }); + await writeFile( + join(packageRoot, "package.json"), + JSON.stringify({ + name: "fixture", + [field]: declaredPath, + }), + ); + await writeFile( + join(packageRoot, target), + `module.exports=${JSON.stringify(target)};`, + ); + const literal = declaredPath.slice(2).split("?", 1)[0]?.split("#", 1)[0]; + if (literal !== undefined && literal !== target) + await writeFile( + join(packageRoot, literal), + 'module.exports="literal decoy";', + ); + const main = join( + root, + moduleKind === "import" ? "main.mjs" : "main.cjs", + ); + await writeFile( + main, + moduleKind === "import" + ? 'import value from "fixture"; console.log(value);' + : 'console.log(require("fixture"));', + ); + const native = await promisify(execFile)(process.execPath, [main], { + timeout: 5_000, + }); + expect(native.stdout.trim()).toBe(target); + const result = await analyzeJavaScriptApplication({ + input_path: root, + format: "directory", + }); + expect(result.ok).toBe(true); + if (!result.ok) throw result.error; + const { graph } = javascriptApplicationAnalysisResultSchema.parse( + result.value.normalized_result, + ); + const imports = graph.edges.filter( + (edge) => + edge.relation === "imports" && + edge.properties.specifier === "fixture", + ); + expect(imports.length).toBeGreaterThan(0); + for (const edge of imports) + expect(edge.properties.resolved_path).toBe( + `node_modules/fixture/${target}`, + ); + }, + ); + + it.each( + (["import", "require"] as const).flatMap((moduleKind) => + [ + "./actual%file.cjs", + "./actual%E0%A4file.cjs", + "./actual%00file.cjs", + "./actual%2Ffile.cjs", + "./actual%5Cfile.cjs", + "./actual%2Efile.cjs", + "./../../../../escape.cjs", + ].map((declaredPath) => ({ moduleKind, declaredPath })), + ), + )( + "rejects $moduleKind URL $declaredPath without borrowing a literal decoy", + ({ moduleKind, declaredPath }) => { + const packagePath = "app/node_modules/fixture/package.json"; + const files = filesFor([ + packagePath, + `app/node_modules/fixture/${declaredPath.slice(2)}`, + ]); + const metadata = files.get(packagePath); + if (metadata === undefined) throw new Error("Missing fixture metadata"); + files.set(packagePath, { + ...metadata, + kind: "package-json", + text: { + included: true, + value: JSON.stringify({ exports: declaredPath }), + }, + }); + expect( + resolveArtifactPathByContext({ + declaredPath: "fixture", + sourcePath: "app/main.js", + context: "module-specifier", + moduleKind, + files, + }), + ).toMatchObject({ resolved_path: null, resolution_status: "rejected" }); + }, + ); + + it.each(["import", "require"] as const)( + "keeps %s exports inside their inventoried container", + (moduleKind) => { + const packagePath = "app/node_modules/fixture/package.json"; + const target = "app/node_modules/fixture/actual file.cjs"; + const files = filesFor([packagePath, target]); + const metadata = files.get(packagePath); + const crossContainer = files.get(target); + if (metadata === undefined || crossContainer === undefined) + throw new Error("Missing fixture files"); + files.set(packagePath, { + ...metadata, + kind: "package-json", + text: { + included: true, + value: '{"exports":"./actual%20file.cjs"}', + }, + }); + files.set(target, { + ...crossContainer, + container_sha256: "c".repeat(64), + }); + expect( + resolveArtifactPathByContext({ + declaredPath: "fixture", + sourcePath: "app/main.js", + context: "module-specifier", + moduleKind, + files, + }), + ).toMatchObject({ resolved_path: null, resolution_status: "not-found" }); + }, + ); +}); diff --git a/src/application/javascript/JavaScriptReturnShapeProjection.test.ts b/src/application/javascript/JavaScriptReturnShapeProjection.test.ts deleted file mode 100644 index 11d91c02f9..0000000000 --- a/src/application/javascript/JavaScriptReturnShapeProjection.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; -import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; -import { projectJavaScriptExportReturnShapes } from "./JavaScriptReturnShapeProjection.js"; - -const projectReturns = (body: string) => { - const ir = analyzeJavaScriptSemantics(`export function result() { ${body} }`); - const link = ir.moduleLinks.find( - ({ exportedName }) => exportedName === "result", - ); - if (link === undefined) throw new Error("Expected the result export"); - const projected = projectJavaScriptExportReturnShapes({ - ir, - link, - modulePath: "fixture.js", - baseCoverage: completeApplicationCoverage(), - }); - if (projected === null) throw new Error("Expected exported return shapes"); - return projected; -}; - -const unknownBranchField = { - path: "", - state: "unknown", - value: null, - reason: "Branches have incompatible values.", -}; - -describe("exported primitive union return shapes", () => { - const conditionalCases = [ - "missing", - "unsupported()", - '({ key: "value" })', - '["value"]', - '+"invalid"', - ].flatMap((other) => [ - `condition ? (choice ? "one" : "two") : ${other}`, - `condition ? ${other} : (choice ? "one" : "two")`, - ]); - const logicalCases = [ - { operator: "||", known: '(choice ? "" : "one")' }, - { operator: "&&", known: '(choice ? 0 : "one")' }, - { operator: "??", known: '(choice ? null : "one")' }, - ].flatMap(({ operator, known }) => [ - `${known} ${operator} missing`, - `missing ${operator} ${known}`, - ]); - - it.each([...conditionalCases, ...logicalCases])( - "projects an unknown field instead of a definite union for %s", - (expression) => { - const projected = projectReturns(`return ${expression};`); - - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "ambiguous", - fields: [unknownBranchField], - property_coverage: [], - }, - ]); - // Complete return-site collection does not make its values known. - expect(projected.properties.return_shape_coverage).toEqual({ - status: "complete", - retained_return_sites: 1, - omitted_return_sites: 0, - omitted_fields: 0, - omitted_property_coverage: 0, - projection_complete: true, - }); - expect(projected.coverage).toEqual(completeApplicationCoverage()); - expect(projected.limitations).toContain( - "Return shapes are inferred from inert syntax and do not prove runtime behavior.", - ); - }, - ); - - it("keeps an incompatible object field unknown beside a known literal", () => { - const projected = projectReturns(` - return { - kind: "result", - value: condition ? (choice ? "one" : "two") : missing, - }; - `); - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "object", - fields: [ - { path: "/kind", state: "literal", value: "result", reason: null }, - { ...unknownBranchField, path: "/value" }, - ], - property_coverage: [{ path: "", status: "complete", omitted: 0 }], - }, - ]); - }); - - it("retains valid nested unions, duplicates, and null alternatives", () => { - const projected = projectReturns(` - return condition - ? (choice ? "two" : "one") - : (other ? "one" : null); - `); - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "union", - fields: [ - { - path: "", - state: "union", - value: [null, "one", "two"], - reason: null, - }, - ], - property_coverage: [], - }, - ]); - }); - - it("preserves three independent direct returns without merging their values", () => { - const projected = projectReturns(` - if (condition) return choice ? "one" : "two"; - if (other) return missing; - return { kind: "object" }; - `); - expect(projected.properties.static_return_shapes).toEqual([ - { - source_range: expect.any(Object), - value_status: "union", - fields: [ - { path: "", state: "union", value: ["one", "two"], reason: null }, - ], - property_coverage: [], - }, - { - source_range: expect.any(Object), - value_status: "unknown", - fields: [ - { - path: "", - state: "unknown", - value: null, - reason: "Unbound identifier missing.", - }, - ], - property_coverage: [], - }, - { - source_range: expect.any(Object), - value_status: "object", - fields: [ - { path: "/kind", state: "literal", value: "object", reason: null }, - ], - property_coverage: [{ path: "", status: "complete", omitted: 0 }], - }, - ]); - expect(projected.properties.return_shape_coverage).toMatchObject({ - status: "complete", - retained_return_sites: 3, - omitted_return_sites: 0, - projection_complete: true, - }); - }); -}); diff --git a/src/application/javascript/JavaScriptReturnShapeProjection.ts b/src/application/javascript/JavaScriptReturnShapeProjection.ts index dcd806ef88..f22a5fecaf 100644 --- a/src/application/javascript/JavaScriptReturnShapeProjection.ts +++ b/src/application/javascript/JavaScriptReturnShapeProjection.ts @@ -4,13 +4,13 @@ import { truncatedApplicationCoverage, } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import type { - JavaScriptSemanticIr, JavaScriptSemanticCallable, JavaScriptSemanticModuleLink, } from "../../domain/javascript/javascriptSemanticIr.js"; import { flattenSemanticReturnValue } from "../../domain/javascript/javascriptSemanticReturns.js"; import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; import type { JavaScriptArtifactGraphCoverage } from "./JavaScriptArtifactGraphContext.js"; +import type { JavaScriptModuleSemanticIr } from "./JavaScriptArtifactAnalysisTypes.js"; export interface JavaScriptReturnShapeProjection { readonly properties: ReturnType; @@ -21,7 +21,7 @@ export interface JavaScriptReturnShapeProjection { /** Project one exact export/callable link into shallow bounded graph values. */ export const projectJavaScriptExportReturnShapes = (input: { - readonly ir: JavaScriptSemanticIr; + readonly ir: JavaScriptModuleSemanticIr; readonly link: JavaScriptSemanticModuleLink; readonly modulePath: string; readonly baseCoverage: JavaScriptArtifactGraphCoverage; diff --git a/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts b/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts index c44b1cc5ac..8d949e964f 100644 --- a/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts @@ -5,10 +5,9 @@ import type { JavaScriptSemanticTimerOperation, } from "../../domain/javascript/javascriptSemanticIr.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, addSemanticGraphUnknown, - constructSemanticGraphNode, } from "./JavaScriptSemanticGraphConstruction.js"; import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; import { @@ -59,55 +58,41 @@ const addEventNode = ( operation: JavaScriptSemanticEventOperation, eventKey: string, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "event", - roleKey: `event:${eventKey}`, - location: operation.eventName === null ? operation.location : null, - label: operation.eventName, - functionNodeId: null, - properties: { - emitter_key: operation.emitterKey, - event_name: operation.eventName, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "event", + roleKey: `event:${eventKey}`, + location: operation.eventName === null ? operation.location : null, + label: operation.eventName, + functionNodeId: null, + properties: { + emitter_key: operation.emitterKey, + event_name: operation.eventName, + }, + }); const addListenerNode = ( context: SemanticFlowProjectionContext, operation: JavaScriptSemanticEventOperation, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "listener", - roleKey: `listener:${operation.eventId}`, - location: operation.listenerLocation ?? operation.location, - label: - operation.eventName === null - ? operation.method - : `${operation.method}:${operation.eventName}`, - functionNodeId: - operation.ownerCallableId === null - ? null - : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? - null), - properties: { - emitter_key: operation.emitterKey, - event_name: operation.eventName, - method: operation.method, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "listener", + roleKey: `listener:${operation.eventId}`, + location: operation.listenerLocation ?? operation.location, + label: + operation.eventName === null + ? operation.method + : `${operation.method}:${operation.eventName}`, + functionNodeId: + operation.ownerCallableId === null + ? null + : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? + null), + properties: { + emitter_key: operation.emitterKey, + event_name: operation.eventName, + method: operation.method, + }, + }); const eventEmitterNode = ( context: SemanticFlowProjectionContext, @@ -164,24 +149,17 @@ export const projectSemanticTimers = ( operation.ownerCallableId === null ? undefined : context.callableNodes.get(operation.ownerCallableId); - const timer = addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "timer", - roleKey: operation.timerId, - location: operation.location, - label: operation.method, - functionNodeId: owner?.node_id ?? null, - properties: { - delay_milliseconds: operation.delayMilliseconds, - method: operation.method, - }, - }, - context.state, - ), - ); + const timer = retainSemanticGraphNode(context.state, context.file, { + kind: "timer", + roleKey: operation.timerId, + location: operation.location, + label: operation.method, + functionNodeId: owner?.node_id ?? null, + properties: { + delay_milliseconds: operation.delayMilliseconds, + method: operation.method, + }, + }); return timer === null ? [] : [[operation.timerId, timer] as const]; }), ); diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts index e7d1c1240a..2707b06d21 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts @@ -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); +}); diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index 68f4a9ed80..d328eb5a76 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -38,16 +38,17 @@ import { type SemanticFlowProjectionContext, } from "./JavaScriptSemanticGraphFlowProjection.js"; import { - owningSemanticCallableNode, + createSemanticCallableOwnerLookup, + createSemanticCallSiteLookup, + createSemanticNodeRangeLookup, semanticFamilyStatus, - semanticNodesWithinRange, } from "./JavaScriptSemanticGraphProjection.js"; import { addSemanticFallbackRoot as addFallbackRoot, - addSemanticGraphNode as addNode, + retainSemanticGraphNode as retainNode, addSemanticGraphRelation as addRelation, - constructSemanticGraphNode as semanticNode, createSemanticGraphProjectionState as emptyState, + bindSemanticGraphApplicationNodes, type SemanticGraphProjectionState as BuilderState, } from "./JavaScriptSemanticGraphConstruction.js"; @@ -74,137 +75,222 @@ interface FileContext extends SemanticFlowProjectionContext { readonly argumentNodes: Map; readonly referenceNodes: JavaScriptSemanticGraphNode[]; readonly callResolutions: ReadonlyMap; + readonly callableOwnerAt: ReturnType< + typeof createSemanticCallableOwnerLookup + >; } -/** 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, analysis, sourceLimitations = [], }: BuilderInput): JavaScriptSemanticGraph => { - const state = emptyState(applicationGraph); - const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = []; + const projection = createJavaScriptSemanticGraphProjection(); for (const analyzed of analysis.files) { - if (analyzed.semantic === null) continue; - fingerprints.push( - ...projectFile(analyzed.file, analyzed.semantic.ir, state), - ); + if (analyzed.semantic !== null) + projection.projectFile(analyzed.file, analyzed.semantic.ir); } - if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state); - const unknowns = [...state.unknowns.values()]; - return createJavaScriptSemanticGraph({ - schema: "JavaScriptSemanticRelationGraph", - root_artifact_sha256: rootArtifactSha256, - application_graph_id: applicationGraph.graph_id, - root_node_ids: [...state.roots], - nodes: [...state.nodes.values()], - relations: [...state.relations.values()], - fingerprints, - unknowns, - coverage: { - status: "unknown", - truncated: false, - omitted_nodes: 0, - omitted_relations: 0, - limits: [], - families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({ - family, - status: semanticFamilyStatus(family, analysis), - retained_relations: [...state.relations.values()].filter( - (relation) => - JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] === family, - ).length, - omitted_relations: 0, - unknown_ids: unknowns - .filter((unknown) => unknown.family === family) - .map(({ unknown_id: identifier }) => identifier), - })), - }, - limitations: [ - ...sourceLimitations, - "The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.", - "Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.", - "Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.", - "Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.", - "Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.", - "Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.", - "Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.", - "Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.", - "Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.", - "Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.", - "Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.", - ], - }); + return projection.finish( + rootArtifactSha256, + applicationGraph, + analysis, + sourceLimitations, + ); }; +/** File-local semantic projection that does not retain consumed source IR. */ +export interface JavaScriptSemanticGraphProjection { + readonly projectFile: ( + file: JavaScriptArtifactFile, + ir: JavaScriptSemanticIr, + ) => void; + readonly finish: ( + rootArtifactSha256: string, + applicationGraph: BuilderInput["applicationGraph"], + analysis: Pick, + sourceLimitations?: readonly string[], + ) => JavaScriptSemanticGraph; +} + +/** Accumulate semantic nodes while each source IR is still file-local. */ +export const createJavaScriptSemanticGraphProjection = + (): JavaScriptSemanticGraphProjection => { + const state = emptyState({ nodes: [] }); + const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = []; + let truncatedFiles = 0; + const projectSource = ( + file: JavaScriptArtifactFile, + ir: JavaScriptSemanticIr, + ): void => { + if (state.nodes.size >= SEMANTIC_GRAPH_NODE_CEILING) { + truncatedFiles += 1; + return; + } + 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(file, 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; + }; + return { + projectFile: projectSource, + finish: ( + rootArtifactSha256, + applicationGraph, + analysis, + sourceLimitations = [], + ) => { + bindSemanticGraphApplicationNodes(state, applicationGraph); + if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state); + const unknowns = [...state.unknowns.values()]; + const graph = createJavaScriptSemanticGraph({ + schema: "JavaScriptSemanticRelationGraph", + root_artifact_sha256: rootArtifactSha256, + application_graph_id: applicationGraph.graph_id, + root_node_ids: [...state.roots], + nodes: [...state.nodes.values()], + relations: [...state.relations.values()], + fingerprints, + unknowns, + coverage: { + 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), + retained_relations: [...state.relations.values()].filter( + (relation) => + JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] === + family, + ).length, + omitted_relations: truncatedFiles > 0 ? null : 0, + unknown_ids: unknowns + .filter((unknown) => unknown.family === family) + .map(({ unknown_id: identifier }) => identifier), + })), + }, + limitations: [ + ...sourceLimitations, + "The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.", + "Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.", + "Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.", + "Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.", + "Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.", + "Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.", + "Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.", + "Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.", + "Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.", + "Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.", + "Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.", + ], + }); + state.nodes.clear(); + state.relations.clear(); + state.unknowns.clear(); + state.roots.clear(); + fingerprints.length = 0; + return graph; + }, + }; + }; + const projectFile = ( file: JavaScriptArtifactFile, ir: JavaScriptSemanticIr, state: BuilderState, ): JavaScriptSemanticGraph["fingerprints"][number][] => { - const moduleNode = addNode( - state, - semanticNode( - file, - { - kind: "module", - roleKey: "module", - location: null, - label: file.path, - functionNodeId: null, - }, - state, - ), - ); + const moduleNode = retainNode(state, file, { + kind: "module", + roleKey: "module", + location: null, + label: file.path, + functionNodeId: null, + }); if (moduleNode === null) return []; state.roots.add(moduleNode.node_id); const callableNodes = new Map( ir.callables.flatMap((callable) => { - const node = addNode( - state, - semanticNode( - file, - { - kind: "function", - roleKey: `callable:${callable.callableId}`, - location: callable.location, - label: callable.name, - functionNodeId: null, - // The label is display text; keep the exact name, which may be "". - properties: { name: callable.name }, - }, - state, - ), - ); + const node = retainNode(state, file, { + kind: "function", + roleKey: `callable:${callable.callableId}`, + location: callable.location, + label: callable.name, + functionNodeId: null, + // The label is display text; keep the exact name, which may be "". + properties: { name: callable.name }, + }); return node === null ? [] : [[callable.callableId, node] as const]; }), ); + const callableOwnerAt = createSemanticCallableOwnerLookup(ir, callableNodes); const bindingNodes = new Map( ir.bindings.flatMap((binding) => { const location = binding.definitions[0]?.location ?? null; const kind = binding.kind === "parameter" ? "parameter" : "binding"; - const owner = owningSemanticCallableNode(location, ir, callableNodes); - const node = addNode( - state, - semanticNode( - file, - { - kind, - roleKey: `binding:${binding.bindingId}`, - location, - label: binding.name, - functionNodeId: owner?.node_id ?? null, - }, - state, - ), - ); + const owner = callableOwnerAt(location); + const node = retainNode(state, file, { + kind, + roleKey: `binding:${binding.bindingId}`, + location, + label: binding.name, + functionNodeId: owner?.node_id ?? null, + }); return node === null ? [] : [[binding.bindingId, node] as const]; }), ); const returnSiteNodes = createReturnSiteNodes(file, ir, callableNodes, state); const callSiteNodes = createCallSiteNodes(file, ir, callableNodes, state); - const context: FileContext = { + const definitions: Omit = { file, ir, state, @@ -221,8 +307,16 @@ const projectFile = ( call.resolution === "exact" ? "resolved" : "candidate", ]), ), + callableOwnerAt, + callSiteAt: createSemanticCallSiteLookup(ir, callSiteNodes), + }; + projectDefinitionsAndReferences(definitions); + const context: FileContext = { + ...definitions, + referenceNodesWithin: createSemanticNodeRangeLookup( + definitions.referenceNodes, + ), }; - projectDefinitionsAndReferences(context); projectSemanticValues(context); projectSemanticObjects(context); projectCalls(context); @@ -251,20 +345,13 @@ const createReturnSiteNodes = ( if (owner === undefined) continue; for (const site of callable.returnSites) { const identifier = site.returnSiteId; - const node = addNode( - state, - semanticNode( - file, - { - kind: "return-site", - roleKey: identifier, - location: site.location, - label: "return", - functionNodeId: owner.node_id, - }, - state, - ), - ); + const node = retainNode(state, file, { + kind: "return-site", + roleKey: identifier, + location: site.location, + label: "return", + functionNodeId: owner.node_id, + }); if (node !== null) result.set(identifier, node); } } @@ -283,44 +370,32 @@ const createCallSiteNodes = ( call.callerCallableId === null ? null : (callables.get(call.callerCallableId)?.node_id ?? null); - const node = addNode( - state, - semanticNode( - file, - { - kind: "call-site", - roleKey: `call:${call.callSiteId}`, - location: call.location, - label: call.kind, - functionNodeId: owner, - }, - state, - ), - ); + const node = retainNode(state, file, { + kind: "call-site", + roleKey: `call:${call.callSiteId}`, + location: call.location, + label: call.kind, + functionNodeId: owner, + }); if (node !== null) result.set(call.callSiteId, node); } return result; }; -const projectDefinitionsAndReferences = (context: FileContext): void => { +const projectDefinitionsAndReferences = ( + context: Omit, +): void => { for (const binding of context.ir.bindings) { const bindingNode = context.bindingNodes.get(binding.bindingId); if (bindingNode === undefined) continue; for (const [index, definition] of binding.definitions.entries()) { - const definitionNode = addNode( - context.state, - semanticNode( - context.file, - { - kind: "expression", - roleKey: `definition:${binding.bindingId}:${String(index)}`, - location: definition.location, - label: definition.kind, - functionNodeId: bindingNode.function_node_id, - }, - context.state, - ), - ); + const definitionNode = retainNode(context.state, context.file, { + kind: "expression", + roleKey: `definition:${binding.bindingId}:${String(index)}`, + location: definition.location, + label: definition.kind, + functionNodeId: bindingNode.function_node_id, + }); addRelation(context.state, { source: definitionNode, target: bindingNode, @@ -330,25 +405,14 @@ const projectDefinitionsAndReferences = (context: FileContext): void => { } } for (const [index, reference] of context.ir.references.entries()) { - const expression = addNode( - context.state, - semanticNode( - context.file, - { - kind: "expression", - roleKey: `reference:${String(index)}:${reference.role}:${reference.name}`, - location: reference.location, - label: reference.name, - functionNodeId: - owningSemanticCallableNode( - reference.location, - context.ir, - context.callableNodes, - )?.node_id ?? null, - }, - context.state, - ), - ); + const expression = retainNode(context.state, context.file, { + kind: "expression", + roleKey: `reference:${String(index)}:${reference.role}:${reference.name}`, + location: reference.location, + label: reference.name, + functionNodeId: + context.callableOwnerAt(reference.location)?.node_id ?? null, + }); const binding = reference.bindingId === null ? undefined @@ -398,10 +462,7 @@ const projectCalls = (context: FileContext): void => { `${call.callSiteId}\u0000${String(argument.index)}`, argumentNode, ); - const references = semanticNodesWithinRange( - context.referenceNodes, - argument.location, - ); + const references = context.referenceNodesWithin(argument.location); for (const reference of references) addRelation(context.state, { source: reference, @@ -450,19 +511,12 @@ const createArgumentNode = ( callSiteId: string, argument: JavaScriptSemanticCallArgument, ): JavaScriptSemanticGraphNode | null => - addNode( - context.state, - semanticNode( - context.file, - { - kind: "expression", - roleKey: `argument:${callSiteId}:${String(argument.index)}`, - location: argument.location, - label: argument.spread - ? "spread argument" - : `argument ${String(argument.index)}`, - functionNodeId: callNode.function_node_id, - }, - context.state, - ), - ); + retainNode(context.state, context.file, { + kind: "expression", + roleKey: `argument:${callSiteId}:${String(argument.index)}`, + location: argument.location, + label: argument.spread + ? "spread argument" + : `argument ${String(argument.index)}`, + functionNodeId: callNode.function_node_id, + }); diff --git a/src/application/javascript/JavaScriptSemanticGraphChildProcessProjection.ts b/src/application/javascript/JavaScriptSemanticGraphChildProcessProjection.ts index 450ecff565..600d0390bc 100644 --- a/src/application/javascript/JavaScriptSemanticGraphChildProcessProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphChildProcessProjection.ts @@ -5,10 +5,9 @@ import type { JavaScriptSemanticChildProcessSpawn, } from "../../domain/javascript/javascriptSemanticIr.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, addSemanticGraphUnknown, - constructSemanticGraphNode, } from "./JavaScriptSemanticGraphConstruction.js"; import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; import { @@ -53,31 +52,23 @@ const addChildProcessNode = ( context: SemanticFlowProjectionContext, spawn: JavaScriptSemanticChildProcessSpawn, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "child-process", - roleKey: spawn.processId, - location: spawn.location, - label: spawn.command ?? spawn.method, - functionNodeId: - spawn.ownerCallableId === null - ? null - : (context.callableNodes.get(spawn.ownerCallableId)?.node_id ?? - null), - properties: { - argv_count: spawn.argvCount, - command: spawn.command, - environment_supplied: spawn.environmentSupplied, - method: spawn.method, - stdio_mode: spawn.stdioMode, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "child-process", + roleKey: spawn.processId, + location: spawn.location, + label: spawn.command ?? spawn.method, + functionNodeId: + spawn.ownerCallableId === null + ? null + : (context.callableNodes.get(spawn.ownerCallableId)?.node_id ?? null), + properties: { + argv_count: spawn.argvCount, + command: spawn.command, + environment_supplied: spawn.environmentSupplied, + method: spawn.method, + stdio_mode: spawn.stdioMode, + }, + }); const projectSpawn = ( context: SemanticFlowProjectionContext, @@ -141,21 +132,14 @@ const addStdioNode = ( spawn: JavaScriptSemanticChildProcessSpawn, child: JavaScriptSemanticGraphNode, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "stdio", - roleKey: `stdio:${spawn.processId}`, - location: spawn.location, - label: spawn.stdioMode, - functionNodeId: child.function_node_id, - properties: { mode: spawn.stdioMode }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "stdio", + roleKey: `stdio:${spawn.processId}`, + location: spawn.location, + label: spawn.stdioMode, + functionNodeId: child.function_node_id, + properties: { mode: spawn.stdioMode }, + }); const projectInteraction = ( context: SemanticFlowProjectionContext, @@ -204,52 +188,38 @@ const addChildListenerNode = ( context: SemanticFlowProjectionContext, interaction: JavaScriptSemanticChildProcessInteraction, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "listener", - roleKey: interaction.interactionId, - location: interaction.listenerLocation ?? interaction.location, - label: interaction.eventName, - functionNodeId: - interaction.ownerCallableId === null - ? null - : (context.callableNodes.get(interaction.ownerCallableId) - ?.node_id ?? null), - properties: { - event_name: interaction.eventName, - method: interaction.method, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "listener", + roleKey: interaction.interactionId, + location: interaction.listenerLocation ?? interaction.location, + label: interaction.eventName, + functionNodeId: + interaction.ownerCallableId === null + ? null + : (context.callableNodes.get(interaction.ownerCallableId)?.node_id ?? + null), + properties: { + event_name: interaction.eventName, + method: interaction.method, + }, + }); const addSignalNode = ( context: SemanticFlowProjectionContext, interaction: JavaScriptSemanticChildProcessInteraction, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "signal", - roleKey: interaction.interactionId, - location: interaction.location, - label: interaction.signalName, - functionNodeId: - interaction.ownerCallableId === null - ? null - : (context.callableNodes.get(interaction.ownerCallableId) - ?.node_id ?? null), - properties: { signal_name: interaction.signalName }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "signal", + roleKey: interaction.interactionId, + location: interaction.location, + label: interaction.signalName, + functionNodeId: + interaction.ownerCallableId === null + ? null + : (context.callableNodes.get(interaction.ownerCallableId)?.node_id ?? + null), + properties: { signal_name: interaction.signalName }, + }); const addChildUnknown = (input: ChildUnknownInput): void => { const { context, node, location, relationKinds, detail, candidateNodeIds } = diff --git a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts index a19f749826..bb7322783a 100644 --- a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts +++ b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts @@ -4,6 +4,7 @@ import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValu import { createJavaScriptSemanticGraphNode, createJavaScriptSemanticGraphRelation, + javaScriptSemanticNodeId, type JavaScriptSemanticGraphNode, } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; @@ -26,6 +27,24 @@ export interface SemanticGraphProjectionState { readonly unknowns: Map; readonly roots: Set; readonly applicationNodeIdsByLocation: ReadonlyMap; + /** + * 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. */ @@ -62,6 +81,28 @@ export const createSemanticGraphProjectionState = ( }; }; +/** Bind retained semantic identities after all application observations exist. */ +export const bindSemanticGraphApplicationNodes = ( + state: SemanticGraphProjectionState, + applicationGraph: Pick, +): void => { + const index = indexApplicationNodes(applicationGraph.nodes).identifiers; + for (const [identifier, node] of state.nodes) { + state.nodes.set(identifier, { + ...node, + application_node_ids: [ + ...(index.get( + applicationLocationKey( + node.identity.artifact_sha256, + node.identity.module_path, + node.identity.source_range, + ), + ) ?? []), + ], + }); + } +}; + /** Construct one canonical semantic node backed by an exact artifact file. */ export const constructSemanticGraphNode = ( file: JavaScriptArtifactFile, @@ -83,6 +124,33 @@ export const constructSemanticGraphNode = ( evidence: observedSemanticEvidence(file, input.location), }); +/** Reuse retained identities and reject exhausted budgets before allocation. */ +export const retainSemanticGraphNode = ( + state: SemanticGraphProjectionState, + file: JavaScriptArtifactFile, + input: SemanticNodeConstructionInput, +): JavaScriptSemanticGraphNode | null => { + const identifier = javaScriptSemanticNodeId({ + kind: input.kind, + identity: { + artifact_sha256: file.sha256, + module_path: file.path, + source_range: input.location, + role_key: input.roleKey, + }, + }); + const existing = state.nodes.get(identifier); + if (existing !== undefined) return existing; + if (typeof state.fileNodeBudget === "number" && state.fileNodeBudget <= 0) { + state.fileNodesDropped = true; + return null; + } + return addSemanticGraphNode( + state, + constructSemanticGraphNode(file, input, state), + ); +}; + /** Retain one canonical semantic node. */ export const addSemanticGraphNode = ( state: SemanticGraphProjectionState, @@ -90,6 +158,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; }; diff --git a/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts b/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts index 8ebd024b89..761eba0e32 100644 --- a/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts @@ -6,10 +6,9 @@ import type { JavaScriptSemanticRequestOperation, } from "../../domain/javascript/javascriptSemanticIr.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, addSemanticGraphUnknown, - constructSemanticGraphNode, } from "./JavaScriptSemanticGraphConstruction.js"; import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; import { @@ -70,29 +69,22 @@ const addConfigurationNode = ( context: SemanticFlowProjectionContext, operation: JavaScriptSemanticConfigurationOperation, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "config-source", - roleKey: operation.configId, - location: operation.location, - label: operation.key ?? operation.kind, - functionNodeId: - operation.ownerCallableId === null - ? null - : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? - null), - properties: { - key: operation.key, - source_kind: operation.kind, - value: operation.value, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "config-source", + roleKey: operation.configId, + location: operation.location, + label: operation.key ?? operation.kind, + functionNodeId: + operation.ownerCallableId === null + ? null + : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? + null), + properties: { + key: operation.key, + source_kind: operation.kind, + value: operation.value, + }, + }); const configurationRelation = ( operation: JavaScriptSemanticConfigurationOperation, @@ -144,29 +136,22 @@ const addRequestNode = ( context: SemanticFlowProjectionContext, operation: JavaScriptSemanticRequestOperation, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: operation.kind === "request" ? "request" : "response", - roleKey: operation.requestId, - location: operation.location, - label: operation.endpoint ?? operation.method, - functionNodeId: - operation.ownerCallableId === null - ? null - : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? - null), - properties: { - endpoint: operation.endpoint, - method: operation.method, - operation_kind: operation.kind, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: operation.kind === "request" ? "request" : "response", + roleKey: operation.requestId, + location: operation.location, + label: operation.endpoint ?? operation.method, + functionNodeId: + operation.ownerCallableId === null + ? null + : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? + null), + properties: { + endpoint: operation.endpoint, + method: operation.method, + operation_kind: operation.kind, + }, + }); const projectRequestConstruction = ( context: SemanticFlowProjectionContext, @@ -265,28 +250,21 @@ const addBoundaryNode = ( context: SemanticFlowProjectionContext, operation: JavaScriptSemanticBoundaryOperation, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "boundary", - roleKey: operation.boundaryId, - location: operation.location, - label: operation.method, - functionNodeId: - operation.ownerCallableId === null - ? null - : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? - null), - properties: { - boundary_kind: operation.kind, - method: operation.method, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "boundary", + roleKey: operation.boundaryId, + location: operation.location, + label: operation.method, + functionNodeId: + operation.ownerCallableId === null + ? null + : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? + null), + properties: { + boundary_kind: operation.kind, + method: operation.method, + }, + }); const bindingOrOwner = ( context: SemanticFlowProjectionContext, diff --git a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts index fba83df6b0..b8c685bc54 100644 --- a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts @@ -5,17 +5,15 @@ import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnaly import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, addSemanticGraphUnknown, - constructSemanticGraphNode, type SemanticGraphProjectionState, } from "./JavaScriptSemanticGraphConstruction.js"; import { inferredSemanticEvidenceAt, unknownSemanticEvidence, } from "./JavaScriptSemanticGraphEvidence.js"; -import { semanticNodesWithinRange } from "./JavaScriptSemanticGraphProjection.js"; /** File-local graph state needed by return, capture, and frontier projection. */ export interface SemanticFlowProjectionContext { @@ -28,6 +26,12 @@ export interface SemanticFlowProjectionContext { readonly callSiteNodes: ReadonlyMap; readonly returnSiteNodes: ReadonlyMap; readonly referenceNodes: readonly JavaScriptSemanticGraphNode[]; + readonly referenceNodesWithin: ( + range: JavaScriptSourceRange, + ) => JavaScriptSemanticGraphNode[]; + readonly callSiteAt: ( + range: JavaScriptSourceRange, + ) => JavaScriptSemanticGraphNode | undefined; } /** Find the retained call-site node at one exact semantic source range. */ @@ -35,12 +39,7 @@ export const semanticCallSiteAt = ( context: SemanticFlowProjectionContext, location: JavaScriptSourceRange, ): JavaScriptSemanticGraphNode | undefined => { - const call = context.ir.callSites.find(({ location: candidate }) => - sourceRangesEqual(candidate, location), - ); - return call === undefined - ? undefined - : context.callSiteNodes.get(call.callSiteId); + return context.callSiteAt(location); }; /** Project explicit static Promise/task ownership without runtime claims. */ @@ -53,26 +52,19 @@ export const projectSemanticPromises = ( operation.ownerCallableId === null ? undefined : context.callableNodes.get(operation.ownerCallableId); - const node = addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: operation.kind === "awaited-expression" ? "task" : "promise", - roleKey: operation.promiseId, - location: operation.location, - label: promiseLabel(operation), - functionNodeId: owner?.node_id ?? null, - properties: { - method: operation.method, - operation_kind: operation.kind, - ownership: operation.ownership, - source_resolution: operation.sourceResolution, - }, - }, - context.state, - ), - ); + const node = retainSemanticGraphNode(context.state, context.file, { + kind: operation.kind === "awaited-expression" ? "task" : "promise", + roleKey: operation.promiseId, + location: operation.location, + label: promiseLabel(operation), + functionNodeId: owner?.node_id ?? null, + properties: { + method: operation.method, + operation_kind: operation.kind, + ownership: operation.ownership, + source_resolution: operation.sourceResolution, + }, + }); return node === null ? [] : [[operation.promiseId, node] as const]; }), ); @@ -217,10 +209,7 @@ export const projectSemanticReturnValues = ( for (const callable of context.ir.callables) for (const site of callable.returnSites) { const returnNode = context.returnSiteNodes.get(site.returnSiteId); - const references = semanticNodesWithinRange( - context.referenceNodes, - site.location, - ); + const references = context.referenceNodesWithin(site.location); for (const reference of references) addSemanticGraphRelation(context.state, { source: reference, diff --git a/src/application/javascript/JavaScriptSemanticGraphObjectProjection.ts b/src/application/javascript/JavaScriptSemanticGraphObjectProjection.ts index e0a5d8890b..e0e3b78e8a 100644 --- a/src/application/javascript/JavaScriptSemanticGraphObjectProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphObjectProjection.ts @@ -2,10 +2,9 @@ import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/ja import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { JavaScriptSemanticObjectOperation } from "../../domain/javascript/javascriptSemanticIr.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, addSemanticGraphUnknown, - constructSemanticGraphNode, } from "./JavaScriptSemanticGraphConstruction.js"; import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js"; @@ -72,31 +71,24 @@ const addObjectOccurrence = ( context: SemanticFlowProjectionContext, operation: JavaScriptSemanticObjectOperation, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "expression", - roleKey: operation.objectOperationId, - location: operation.location, - label: - operation.propertyName === null - ? operation.kind - : `${operation.kind}:${operation.propertyName}`, - functionNodeId: - operation.ownerCallableId === null - ? null - : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? - null), - properties: { - operation_kind: operation.kind, - property_name: operation.propertyName, - }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "expression", + roleKey: operation.objectOperationId, + location: operation.location, + label: + operation.propertyName === null + ? operation.kind + : `${operation.kind}:${operation.propertyName}`, + functionNodeId: + operation.ownerCallableId === null + ? null + : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? + null), + properties: { + operation_kind: operation.kind, + property_name: operation.propertyName, + }, + }); const addObjectUnknown = ( context: SemanticFlowProjectionContext, diff --git a/src/application/javascript/JavaScriptSemanticGraphProjection.test.ts b/src/application/javascript/JavaScriptSemanticGraphProjection.test.ts new file mode 100644 index 0000000000..086319bdca --- /dev/null +++ b/src/application/javascript/JavaScriptSemanticGraphProjection.test.ts @@ -0,0 +1,259 @@ +import { expect, it } from "vitest"; + +import { compareCodePoints } from "../../domain/canonicalOrdering.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; +import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; +import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js"; +import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; +import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; +import { + constructSemanticGraphNode, + createSemanticGraphProjectionState, +} from "./JavaScriptSemanticGraphConstruction.js"; +import { + createSemanticCallableOwnerLookup, + createSemanticNodeRangeLookup, + createSemanticCallSiteLookup, +} from "./JavaScriptSemanticGraphProjection.js"; +import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; + +const fixtureFile: JavaScriptArtifactFile = { + path: "app.js", + container_sha256: "a".repeat(64), + sha256: "b".repeat(64), + bytes: 0, + inventory_artifact_id: "fixture-app", + kind: "javascript", + unpacked: false, + text: { included: true, value: "" }, +}; + +const nodesFor = (ir: JavaScriptSemanticIr) => { + const state = createSemanticGraphProjectionState({ nodes: [] }); + + return new Map( + ir.callables.map((callable) => [ + callable.callableId, + constructSemanticGraphNode( + fixtureFile, + { + kind: "function", + roleKey: callable.callableId, + label: callable.name, + location: callable.location, + functionNodeId: null, + }, + state, + ), + ]), + ); +}; + +// Reference behavior before the optimization, including stable ties and +// fallback when the most deeply nested callable has no retained graph node. +const previousOwner = ( + location: JavaScriptSourceRange | null, + ir: JavaScriptSemanticIr, + nodes: ReadonlyMap, +) => { + if (location === null) return undefined; + const compare = ( + left: JavaScriptSourceRange["start"], + right: JavaScriptSourceRange["start"], + ) => left.line - right.line || left.column - right.column; + const key = (range: JavaScriptSourceRange) => + `${String(range.start.line).padStart(12, "0")}:${String(range.start.column).padStart(12, "0")}`; + return ir.callables + .filter( + (callable) => + compare(callable.location.start, location.start) <= 0 && + compare(callable.location.end, location.end) >= 0, + ) + .sort((left, right) => + compareCodePoints(key(right.location), key(left.location)), + ) + .map((callable) => nodes.get(callable.callableId)) + .find((node) => node !== undefined); +}; + +const rangesFor = (ir: JavaScriptSemanticIr) => [ + null, + ...ir.callables.map((callable) => callable.location), + ...ir.bindings.flatMap((binding) => + binding.definitions.map((definition) => definition.location), + ), + ...ir.references.map((reference) => reference.location), + { start: { line: 999, column: 0 }, end: { line: 999, column: 1 } }, +]; + +it("matches the prior owner selection across nested functions, arrows and methods", () => { + const ir = analyzeJavaScriptSemantics(` + const outside = 1; + function outer(parameter) { + function inner(value) { return value + parameter; } + const callback = argument => inner(argument); + return callback(outside); + } + class Example { method(value) { return () => outer(value); } } + `); + const nodes = nodesFor(ir); + const lookup = createSemanticCallableOwnerLookup(ir, nodes); + for (const range of rangesFor(ir)) + expect(lookup(range)).toBe(previousOwner(range, ir, nodes)); +}); + +it("reads retained node membership at query time and keeps file indexes independent", () => { + const ir = analyzeJavaScriptSemantics( + "function outer() { function inner() { return 1; } return inner(); }", + ); + const inner = ir.callables.find((callable) => callable.name === "inner"); + if (inner === undefined) throw new Error("Missing inner fixture callable"); + const nodes = nodesFor(ir); + const otherNodes = nodesFor(ir); + const lookup = createSemanticCallableOwnerLookup(ir, nodes); + const otherLookup = createSemanticCallableOwnerLookup(ir, otherNodes); + const before = lookup(inner.location); + expect(before?.label).toBe("inner"); + nodes.delete(inner.callableId); + expect(lookup(inner.location)?.label).toBe("outer"); + expect(otherLookup(inner.location)?.label).toBe("inner"); + if (before === undefined) throw new Error("Missing retained fixture node"); + nodes.set(inner.callableId, before); + expect(lookup(inner.location)).toBe(before); +}); + +it("preserves stable ties and non-nested overlapping ranges", () => { + const original = analyzeJavaScriptSemantics( + "function first() { return 1; } function second() { return 2; } function third() { return 3; }", + ); + const range = (start: number, end: number): JavaScriptSourceRange => ({ + start: { line: 1, column: start }, + end: { line: 1, column: end }, + }); + const ir = { + ...original, + callables: original.callables.map((callable, index) => ({ + ...callable, + location: index === 0 ? range(0, 90) : range(20, index === 1 ? 60 : 80), + })), + }; + const nodes = nodesFor(ir); + const lookup = createSemanticCallableOwnerLookup(ir, nodes); + for (const query of [ + range(25, 30), + range(25, 70), + range(0, 80), + range(80, 90), + range(91, 92), + ]) + expect(lookup(query)).toBe(previousOwner(query, ir, nodes)); +}); + +it("matches the prior result across a compiled-style collection of sibling callables", () => { + const ir = analyzeJavaScriptSemantics( + Array.from( + { length: 300 }, + (_, index) => + `function f${String(index)}(value) { return value + ${String(index)}; }`, + ).join(""), + ); + const nodes = nodesFor(ir); + const lookup = createSemanticCallableOwnerLookup(ir, nodes); + for (const range of rangesFor(ir)) + expect(lookup(range)).toBe(previousOwner(range, ir, nodes)); +}); + +it("returns contained nodes in original order, including repeated and overlapping entries", () => { + const ir = analyzeJavaScriptSemantics(` + function outer(value) { function inner() { return value; } return inner(); } + const sibling = () => outer(1); + `); + const nodes = [...nodesFor(ir).values()].reverse(); + const repeated = nodes[0]; + if (repeated === undefined) throw new Error("Missing range fixture node"); + nodes.push( + repeated, + constructSemanticGraphNode( + fixtureFile, + { + kind: "module", + roleKey: "module", + location: null, + label: "module", + functionNodeId: null, + }, + createSemanticGraphProjectionState({ nodes: [] }), + ), + ); + const lookup = createSemanticNodeRangeLookup(nodes); + const compare = ( + left: JavaScriptSourceRange["start"], + right: JavaScriptSourceRange["start"], + ) => left.line - right.line || left.column - right.column; + for (const range of rangesFor(ir)) { + if (range === null) continue; + const expected = nodes.filter( + ({ identity }) => + identity.source_range !== null && + compare(range.start, identity.source_range.start) <= 0 && + compare(range.end, identity.source_range.end) >= 0, + ); + expect(lookup(range)).toEqual(expected); + } + expect( + createSemanticNodeRangeLookup([])( + repeated.identity.source_range ?? { + start: { line: 1, column: 0 }, + end: { line: 1, column: 1 }, + }, + ), + ).toEqual([]); +}); + +it("keeps first-call matching and current retained membership for exact locations", () => { + const original = analyzeJavaScriptSemantics( + "function target(value) { return value; } target(1); target(2);", + ); + const first = original.callSites[0]; + const node = [...nodesFor(original).values()][0]; + if (first === undefined || node === undefined) + throw new Error("Missing call fixture"); + const duplicate = { ...first, callSiteId: `${first.callSiteId}:duplicate` }; + const ir = { + ...original, + callSites: [first, duplicate, ...original.callSites.slice(1)], + }; + const nodes = new Map( + ir.callSites.map((call) => [ + call.callSiteId, + constructSemanticGraphNode( + fixtureFile, + { + kind: "call-site", + roleKey: call.callSiteId, + location: call.location, + label: call.callSiteId, + functionNodeId: null, + }, + createSemanticGraphProjectionState({ nodes: [] }), + ), + ]), + ); + const lookup = createSemanticCallSiteLookup(ir, nodes); + const previous = (range: JavaScriptSourceRange) => { + const call = ir.callSites.find((candidate) => + sourceRangesEqual(candidate.location, range), + ); + return call === undefined ? undefined : nodes.get(call.callSiteId); + }; + for (const call of ir.callSites) + expect(lookup(call.location)).toBe(previous(call.location)); + nodes.delete(first.callSiteId); + expect(lookup(first.location)).toBeUndefined(); + expect(nodes.has(duplicate.callSiteId)).toBe(true); + nodes.set(first.callSiteId, node); + expect(lookup(first.location)).toBe(node); + expect( + lookup({ start: { line: 99, column: 0 }, end: { line: 99, column: 1 } }), + ).toBeUndefined(); +}); diff --git a/src/application/javascript/JavaScriptSemanticGraphProjection.ts b/src/application/javascript/JavaScriptSemanticGraphProjection.ts index 4d09591e4a..ac0b4e519c 100644 --- a/src/application/javascript/JavaScriptSemanticGraphProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphProjection.ts @@ -5,40 +5,173 @@ import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSem import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -/** Find the innermost callable that contains one exact source range. */ -export const owningSemanticCallableNode = ( - location: JavaScriptSourceRange | null, +interface SourceInterval { + readonly range: JavaScriptSourceRange; + readonly order: number; +} + +interface CallableInterval extends SourceInterval { + readonly callableId: string; + readonly priority: string; +} + +interface SourceIntervalTree { + readonly entry: Interval; + readonly minimumStart: JavaScriptSourceRange["start"]; + readonly maximumStart: JavaScriptSourceRange["start"]; + readonly minimumEnd: JavaScriptSourceRange["end"]; + readonly maximumEnd: JavaScriptSourceRange["end"]; + readonly left: SourceIntervalTree | null; + readonly right: SourceIntervalTree | null; +} + +/** Index one immutable file IR; read retained callable membership at query time. */ +export const createSemanticCallableOwnerLookup = ( ir: JavaScriptSemanticIr, nodes: ReadonlyMap, -): JavaScriptSemanticGraphNode | undefined => { - if (location === null) return undefined; - return ir.callables - .filter((callable) => contains(callable.location, location)) - .sort((left, right) => - compareCodePoints( - positionKey(right.location), - positionKey(left.location), - ), - ) - .map(({ callableId }) => nodes.get(callableId)) - .find((node) => node !== undefined); +): (( + location: JavaScriptSourceRange | null, +) => JavaScriptSemanticGraphNode | undefined) => { + const entries = ir.callables + .map((callable, order): CallableInterval => ({ + callableId: callable.callableId, + range: callable.location, + order, + priority: positionKey(callable.location), + })) + .sort( + (left, right) => + pointCompare(left.range.start, right.range.start) || + left.order - right.order, + ); + const tree = buildSourceIntervalTree(entries, 0, entries.length); + return (location) => { + if (location === null || tree === null || nodes.size === 0) + return undefined; + const pending = [tree]; + let best: CallableInterval | undefined; + let result: JavaScriptSemanticGraphNode | undefined; + while (pending.length > 0) { + const current = pending.pop(); + if ( + current === undefined || + pointCompare(current.minimumStart, location.start) > 0 || + pointCompare(current.maximumEnd, location.end) < 0 + ) + continue; + const candidate = current.entry; + if (contains(candidate.range, location)) { + const node = nodes.get(candidate.callableId); + const rank = + best === undefined + ? 1 + : compareCodePoints(candidate.priority, best.priority) || + best.order - candidate.order; + if (node !== undefined && rank > 0) { + best = candidate; + result = node; + } + } + if (current.left !== null) pending.push(current.left); + if (current.right !== null) pending.push(current.right); + } + return result; + }; }; -/** Find retained semantic nodes whose exact ranges are within one expression. */ -export const semanticNodesWithinRange = ( +const buildSourceIntervalTree = ( + entries: readonly Interval[], + start: number, + end: number, +): SourceIntervalTree | null => { + if (start >= end) return null; + const middle = Math.floor((start + end) / 2); + const entry = entries[middle]; + if (entry === undefined) return null; + // This recursion follows a balanced index, not the producer's nesting depth. + const left = buildSourceIntervalTree(entries, start, middle); + const right = buildSourceIntervalTree(entries, middle + 1, end); + let minimumEnd = entry.range.end; + let maximumEnd = entry.range.end; + for (const child of [left, right]) { + if (child !== null && pointCompare(child.minimumEnd, minimumEnd) < 0) + minimumEnd = child.minimumEnd; + if (child !== null && pointCompare(child.maximumEnd, maximumEnd) > 0) + maximumEnd = child.maximumEnd; + } + return { + entry, + minimumStart: left?.minimumStart ?? entry.range.start, + maximumStart: right?.maximumStart ?? entry.range.start, + minimumEnd, + maximumEnd, + left, + right, + }; +}; + +/** Index an immutable node list; return contained ranges in producer order. */ +export const createSemanticNodeRangeLookup = ( nodes: readonly JavaScriptSemanticGraphNode[], +): ((range: JavaScriptSourceRange) => JavaScriptSemanticGraphNode[]) => { + const entries = nodes + .flatMap((node, order) => { + const range = node.identity.source_range; + return range === null ? [] : [{ node, range, order }]; + }) + .sort( + (left, right) => + pointCompare(left.range.start, right.range.start) || + left.order - right.order, + ); + const tree = buildSourceIntervalTree(entries, 0, entries.length); + return (range) => { + if (tree === null) return []; + const pending = [tree]; + const matches: (typeof entries)[number][] = []; + while (pending.length > 0) { + const current = pending.pop(); + if ( + current === undefined || + pointCompare(current.maximumStart, range.start) < 0 || + pointCompare(current.minimumEnd, range.end) > 0 + ) + continue; + if (contains(range, current.entry.range)) matches.push(current.entry); + if (current.left !== null) pending.push(current.left); + if (current.right !== null) pending.push(current.right); + } + return matches + .sort((left, right) => left.order - right.order) + .map(({ node }) => node); + }; +}; + +/** Index exact call ranges, preserving the first producer occurrence. */ +export const createSemanticCallSiteLookup = ( + ir: JavaScriptSemanticIr, + nodes: ReadonlyMap, +): (( range: JavaScriptSourceRange, -): JavaScriptSemanticGraphNode[] => - nodes.filter( - (node) => - node.identity.source_range !== null && - contains(range, node.identity.source_range), - ); +) => JavaScriptSemanticGraphNode | undefined) => { + const identifiers = new Map(); + for (const call of ir.callSites) { + const key = rangeKey(call.location); + if (!identifiers.has(key)) identifiers.set(key, call.callSiteId); + } + return (range) => { + const identifier = identifiers.get(rangeKey(range)); + return identifier === undefined ? undefined : nodes.get(identifier); + }; +}; + +const rangeKey = (range: JavaScriptSourceRange): string => + `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; /** Report extractor support without treating missing families as absence. */ export const semanticFamilyStatus = ( family: (typeof JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)[number], - analysis: JavaScriptArtifactAnalysis, + analysis: Pick, ): "complete" | "partial" | "unknown" | "unsupported" => { if ( ![ diff --git a/src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts b/src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts index 22ca8aff6b..8689cc6739 100644 --- a/src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts @@ -2,10 +2,9 @@ import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/ja import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { JavaScriptSemanticResourceOperation } from "../../domain/javascript/javascriptSemanticIr.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, addSemanticGraphUnknown, - constructSemanticGraphNode, } from "./JavaScriptSemanticGraphConstruction.js"; import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; import { @@ -39,25 +38,18 @@ const addResourceNode = ( context: SemanticFlowProjectionContext, operation: JavaScriptSemanticResourceOperation, ): JavaScriptSemanticGraphNode | null => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "resource", - roleKey: operation.resourceId, - location: operation.location, - label: operation.method, - functionNodeId: - operation.ownerCallableId === null - ? null - : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? - null), - properties: { method: operation.method }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "resource", + roleKey: operation.resourceId, + location: operation.location, + label: operation.method, + functionNodeId: + operation.ownerCallableId === null + ? null + : (context.callableNodes.get(operation.ownerCallableId)?.node_id ?? + null), + properties: { method: operation.method }, + }); const projectRelease = ( context: SemanticFlowProjectionContext, diff --git a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts index 96298f3496..bcda8680e5 100644 --- a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts @@ -4,9 +4,8 @@ import type { JavaScriptSemanticValue, } from "../../domain/javascript/javascriptSemanticIr.js"; import { - addSemanticGraphNode, + retainSemanticGraphNode, addSemanticGraphRelation, - constructSemanticGraphNode, } from "./JavaScriptSemanticGraphConstruction.js"; import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js"; @@ -85,22 +84,15 @@ const addLiteralNode = ( value: string | number | boolean | null, role: string, ) => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "literal", - roleKey: `literal:${binding.bindingId}:${role}:${JSON.stringify(value)}`, - location: binding.definitions[0]?.location ?? null, - label: JSON.stringify(value), - functionNodeId: - context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null, - properties: { value }, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "literal", + roleKey: `literal:${binding.bindingId}:${role}:${JSON.stringify(value)}`, + location: binding.definitions[0]?.location ?? null, + label: JSON.stringify(value), + functionNodeId: + context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null, + properties: { value }, + }); /** Create one canonical property slot for a binding/property identity. */ export const semanticPropertySlot = ( @@ -108,22 +100,15 @@ export const semanticPropertySlot = ( objectBindingId: string, name: string, ) => - addSemanticGraphNode( - context.state, - constructSemanticGraphNode( - context.file, - { - kind: "property-slot", - roleKey: `property:${objectBindingId}:${name}`, - location: null, - label: name, - functionNodeId: - context.bindingNodes.get(objectBindingId)?.function_node_id ?? null, - properties: { - name, - object_binding_id: objectBindingId, - } satisfies Readonly>, - }, - context.state, - ), - ); + retainSemanticGraphNode(context.state, context.file, { + kind: "property-slot", + roleKey: `property:${objectBindingId}:${name}`, + location: null, + label: name, + functionNodeId: + context.bindingNodes.get(objectBindingId)?.function_node_id ?? null, + properties: { + name, + object_binding_id: objectBindingId, + } satisfies Readonly>, + }); diff --git a/src/application/ProcessCli.ts b/src/application/process/ProcessCli.ts similarity index 84% rename from src/application/ProcessCli.ts rename to src/application/process/ProcessCli.ts index 82c9c83e59..3d0e537491 100644 --- a/src/application/ProcessCli.ts +++ b/src/application/process/ProcessCli.ts @@ -1,18 +1,19 @@ import { readFile } from "node:fs/promises"; +import { z } from "zod"; -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, @@ -113,6 +114,12 @@ const parseCaptureEvidence = (input: unknown) => { try { evidence = parseEvidence(input); } catch (cause: unknown) { + if (cause instanceof z.ZodError) + throw new AnalysisInputError( + "compare_process_captures", + { cause }, + projectInputIssues(cause.issues, input), + ); throw invalidCaptureEvidence(); } if ( @@ -165,7 +172,11 @@ const readJson = async (path: string): Promise => { ); } try { - const parsed: unknown = JSON.parse(bytes.toString("utf8")); + const text = new TextDecoder("utf-8", { + fatal: true, + ignoreBOM: true, + }).decode(bytes); + const parsed: unknown = JSON.parse(text); return parsed; } catch (cause: unknown) { void cause; diff --git a/src/application/ProcessEvidence.ts b/src/application/process/ProcessEvidence.ts similarity index 76% rename from src/application/ProcessEvidence.ts rename to src/application/process/ProcessEvidence.ts index 50cd033cac..516eec4787 100644 --- a/src/application/ProcessEvidence.ts +++ b/src/application/process/ProcessEvidence.ts @@ -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 = ( diff --git a/src/artifacts/ArtifactProvider.ts b/src/artifacts/ArtifactProvider.ts index a73cab602d..e43bc6ea9d 100644 --- a/src/artifacts/ArtifactProvider.ts +++ b/src/artifacts/ArtifactProvider.ts @@ -8,9 +8,10 @@ import { type ExecutionOptions, } from "../application/AnalysisProvider.js"; import { inspectBundleKeyedArchive } from "./apple/KeyedArchiveReader.js"; +import { traceDylibResolution } from "./apple/DylibResolutionReader.js"; import { basename, dirname } from "node:path"; -import { inventoryArtifact } from "../application/ArtifactInventory.js"; -import { extractArtifact } from "../application/ArtifactExtraction.js"; +import { inventoryArtifact } from "./inventory/ArtifactInventory.js"; +import { extractArtifact } from "./extraction/ArtifactExtraction.js"; import { analyzeInterfaceBuilderBundle } from "./apple/InterfaceBuilderAnalysis.js"; import { analyzeAppleAssetCatalogs } from "./apple/AppleAssetCatalogAnalysis.js"; import { @@ -20,7 +21,10 @@ import { type ArtifactAnalysisOperation, } from "../contracts/artifactToolContracts.js"; import type { BinaryTarget } from "../domain/binaryTarget.js"; -import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisInputError, +} from "../domain/analysisErrorCore.js"; import { ArtifactOperationError } from "../domain/artifactOperationError.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; import type { JsonValue } from "../domain/jsonValue.js"; @@ -33,7 +37,7 @@ import { } from "./ArtifactProviderMetadata.js"; import { createEvidence } from "../domain/evidence.js"; import { createArtifactInspection } from "../domain/artifactInspection.js"; -import { resolveArtifactIntegrityPolicy } from "../application/ArtifactInventory/policy.js"; +import { resolveArtifactIntegrityPolicy } from "./inventory/policy.js"; /** Read-only inventory and exclusively owned extraction provider. */ export class ArtifactProvider implements AnalysisProvider { @@ -115,10 +119,15 @@ class ArtifactClient implements AnalysisClient { parameters.path !== "." && parameters.path !== basename(this.target.path) ) - throw new ArtifactReaderFailure( - "path", - "For an active plist, path must select that archive (omit path or use its basename)", - ); + throw new AnalysisInputError(operation, undefined, [ + { + path: ["path"], + reason: "invalid_value", + message: + "For an active plist, path must select that archive (omit path or use its basename).", + expected: basename(this.target.path), + }, + ]); const bundlePath = standalone ? dirname(this.target.path) : this.target.sourcePath; @@ -153,6 +162,8 @@ class ArtifactClient implements AnalysisClient { if (operation === "inspect_asset_catalog") { return await this.inspectAssetCatalog(parameters, options); } + if (operation === "trace_dylib_resolution") + return await this.traceDylibResolution(parameters, options); if (operation === "extract_artifact") { const parsed = artifactExtractionExecutionSchema.parse(parameters); const result = await extractArtifact( @@ -237,6 +248,40 @@ class ArtifactClient implements AnalysisClient { ); } + private async traceDylibResolution( + parameters: Readonly>, + options?: ExecutionOptions, + ) { + if (this.target.kind !== "executable" || this.target.format !== "mach-o") + throw new ArtifactReaderFailure( + "unavailable", + "trace_dylib_resolution requires an active Mach-O or .app bundle target", + ); + // Only a target opened from an app bundle directory carries its Info.plist; + // a regular file whose name ends in .app is a standalone image. + const bundle = + this.target.bundleInfoPlist === undefined + ? undefined + : this.target.sourcePath; + const result = await traceDylibResolution({ + rootPath: bundle ?? dirname(this.target.path), + targetPath: this.target.path, + targetSha256: this.target.sha256, + enumerateRoots: bundle !== undefined, + parameters, + ...(options?.signal === undefined ? {} : { signal: options.signal }), + }); + return ok( + createAnalysisExecution(result, IDENTITY, { + limitations: result.limitations, + locations: result.images.map(({ path }) => ({ + kind: "artifact-path" as const, + path, + })), + }), + ); + } + private async inspectArtifact( parameters: Readonly>, options?: ExecutionOptions, @@ -318,6 +363,8 @@ const translateFailure = ( cause.details, cause.message, ); + // Caller-selection failures are already typed; keep their correction details. + if (cause instanceof AnalysisInputError) return cause; return new ArtifactOperationError(operation, "io"); }; diff --git a/src/artifacts/ArtifactProviderMetadata.ts b/src/artifacts/ArtifactProviderMetadata.ts index 9cbe0d65e9..3e350508ed 100644 --- a/src/artifacts/ArtifactProviderMetadata.ts +++ b/src/artifacts/ArtifactProviderMetadata.ts @@ -24,7 +24,8 @@ export const artifactCapabilities = ( mutatesArtifact: false, launchesProcess: operation !== "decode_interface_builder" && - operation !== "inspect_keyed_archive", + operation !== "inspect_keyed_archive" && + operation !== "trace_dylib_resolution", mayShowUi: false, mayAccessNetwork: false, mayWriteFilesystem: diff --git a/src/artifacts/AsarArtifactReader.ts b/src/artifacts/AsarArtifactReader.ts index e231dae96c..02ebb1bbeb 100644 --- a/src/artifacts/AsarArtifactReader.ts +++ b/src/artifacts/AsarArtifactReader.ts @@ -1,17 +1,19 @@ -import { createReadStream } from "node:fs"; -import { sep } from "node:path"; +import { constants, createReadStream, type Stats } from "node:fs"; +import { lstat, open, realpath, type FileHandle } from "node:fs/promises"; +import { isAbsolute, join, relative, sep } from "node:path"; import { Readable } from "node:stream"; -import { extractFile, listPackage, statFile, uncache } from "@electron/asar"; +import { getRawHeader, listPackage, statFile, uncache } from "@electron/asar"; import { ArtifactReaderFailure, type ArtifactEntry, type ArtifactReader, } from "./ArtifactReader.js"; +import { closeAsarHandle, readValidatedAsarEntry } from "./AsarEntryStream.js"; /** - * Official Electron ASAR adapter. Individual files remain caller-bounded. + * Official Electron ASAR adapter with range-streamed member reads. * * An entry marked `unpacked` is metadata, not an integrity exemption: Electron * stores its bytes beside the archive in `.unpacked`, and callers must @@ -19,17 +21,33 @@ import { */ export class AsarArtifactReader implements ArtifactReader { readonly format = "asar" as const; + #archiveSize: number | undefined; + #headerSize: number | undefined; + readonly #entries = new Map(); - constructor(private readonly path: string) {} + constructor( + private readonly path: string, + private readonly openUnpackedFile: OpenAsarUnpackedFile = open, + ) {} async *entries(signal?: AbortSignal): AsyncIterable { let paths: string[]; try { // ASAR caches headers by path even after the archive has been replaced. + this.#resetArchiveState(); + this.#entries.clear(); uncache(this.path); paths = listPackage(this.path, { isPack: false }).sort((left, right) => left.localeCompare(right, "en"), ); + this.#headerSize = getRawHeader(this.path).headerSize; + const archiveMetadata = await lstat(this.path); + if (!archiveMetadata.isFile() || archiveMetadata.isSymbolicLink()) + throw new ArtifactReaderFailure( + "format", + "ASAR container is not a regular file", + ); + this.#archiveSize = archiveMetadata.size; } catch (cause: unknown) { throw asarFailure(this.path, "inventory", cause); } @@ -56,7 +74,7 @@ export class AsarArtifactReader implements ArtifactReader { : "link" in metadata ? "symlink" : "file"; - yield { + const entry: ArtifactEntry = { path, kind, declaredSize: "size" in metadata ? metadata.size : null, @@ -74,42 +92,121 @@ export class AsarArtifactReader implements ArtifactReader { limitations: kind === "symlink" ? ["ASAR symlink target was not followed or disclosed."] - : [ - "Official ASAR extraction buffers each individually bounded file.", - ], + : [], adapterKey: providerPath, }; + if (kind === "file" && isAsarFileMetadata(metadata)) + this.#entries.set(providerPath, { + metadata, + entry: copyArtifactEntry(entry), + }); + yield entry; } } - open(entry: ArtifactEntry, signal?: AbortSignal): Promise { + async open(entry: ArtifactEntry, signal?: AbortSignal): Promise { abortIfNeeded(signal); - if (entry.kind !== "file") - return Promise.reject( - new ArtifactReaderFailure("format", "ASAR entry is not a regular file"), + const state = this.#entries.get(entry.adapterKey); + if (state === undefined && entry.kind !== "file") + throw new ArtifactReaderFailure( + "format", + "ASAR entry is not a regular file", ); + if (state === undefined) + throw new ArtifactReaderFailure( + "integrity", + `ASAR entry was not produced by this reader: ${entry.path}`, + ); + if (!sameArtifactEntry(entry, state.entry)) + throw new ArtifactReaderFailure( + "integrity", + `ASAR entry metadata changed since inventory: ${state.entry.path}`, + ); + const { metadata } = state; + const producedEntry = state.entry; + if (producedEntry.unpacked) { + const handle = await this.#openUnpackedEntry(producedEntry); + try { + const observed = await handle.stat(); + abortIfNeeded(signal); + if (!observed.isFile()) + throw new ArtifactReaderFailure( + "path", + `ASAR unpacked entry is not a regular file: ${producedEntry.path}`, + ); + } catch (cause: unknown) { + await handle.close().catch(() => undefined); + throw asarFailure(this.path, `read ${producedEntry.path}`, cause); + } + const source = handle.createReadStream({ + start: 0, + autoClose: true, + }); + return readValidatedAsarEntry( + source, + undefined, + producedEntry.path, + this.path, + signal, + ); + } + const archiveSize = this.#archiveSize; + const headerSize = this.#headerSize; + const offset = + metadata.offset === undefined + ? undefined + : parseArchiveOffset(metadata.offset); + const start = + offset === undefined || headerSize === undefined + ? undefined + : 8 + headerSize + offset; + if ( + archiveSize === undefined || + start === undefined || + !Number.isSafeInteger(start) || + metadata.size < 0 || + !Number.isSafeInteger(metadata.size) || + start > archiveSize || + metadata.size > archiveSize - start + ) + throw new ArtifactReaderFailure( + "format", + `ASAR entry range is outside its container: ${producedEntry.path}`, + ); + let handle: FileHandle | undefined; try { - return Promise.resolve( - Readable.from(extractFile(this.path, entry.adapterKey, false)), + const openedHandle = await open( + this.path, + constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, + ); + handle = openedHandle; + const observed = await openedHandle.stat(); + abortIfNeeded(signal); + if (!observed.isFile() || observed.size !== archiveSize) + throw new ArtifactReaderFailure( + "integrity", + `ASAR container changed before read: ${producedEntry.path}`, + ); + if (metadata.size === 0) { + await closeAsarHandle(() => openedHandle.close(), signal); + handle = undefined; + return Readable.from([]); + } + const source = openedHandle.createReadStream({ + start, + end: start + metadata.size - 1, + autoClose: true, + }); + return readValidatedAsarEntry( + source, + metadata.size, + producedEntry.path, + this.path, + signal, ); } catch (cause: unknown) { - if (entry.unpacked && isMissingFile(cause)) - return Promise.reject( - new ArtifactReaderFailure( - "unavailable", - `ASAR unpacked entry bytes are unavailable: ${entry.path}`, - { cause }, - { - logicalPath: entry.path, - declaredSha256: entry.declaredSha256, - calculatedSha256: null, - unpacked: true, - }, - ), - ); - return Promise.reject( - asarFailure(this.path, `read ${entry.path}`, cause), - ); + await handle?.close().catch(() => undefined); + throw asarFailure(this.path, `read ${producedEntry.path}`, cause); } } @@ -121,14 +218,154 @@ export class AsarArtifactReader implements ArtifactReader { close(): Promise { uncache(this.path); + this.#resetArchiveState(); + this.#entries.clear(); return Promise.resolve(); } provenance(): readonly [] { return []; } + + async #openUnpackedEntry(entry: ArtifactEntry): Promise { + const unpackedRoot = `${this.path}.unpacked`; + try { + const rootMetadata = await lstat(unpackedRoot); + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) + throw new ArtifactReaderFailure( + "path", + "ASAR unpacked companion is not a regular directory", + ); + const canonicalRoot = await realpath(unpackedRoot); + const candidate = join(unpackedRoot, entry.adapterKey); + const canonical = await realpath(candidate); + const relativePath = relative(canonicalRoot, canonical); + if ( + relativePath === ".." || + relativePath.startsWith(`..${sep}`) || + isAbsolute(relativePath) + ) + throw new ArtifactReaderFailure( + "path", + `ASAR unpacked entry escaped its companion directory: ${entry.path}`, + ); + const pathMetadata = await lstat(candidate); + if (!pathMetadata.isFile() || pathMetadata.isSymbolicLink()) + throw new ArtifactReaderFailure( + "path", + `ASAR unpacked entry is not a regular file: ${entry.path}`, + ); + const handle = await this.openUnpackedFile( + canonical, + constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, + ); + const openedMetadata = await handle + .stat() + .catch(async (cause: unknown) => { + await handle.close().catch(() => undefined); + throw cause; + }); + if ( + !openedMetadata.isFile() || + openedMetadata.dev !== pathMetadata.dev || + openedMetadata.ino !== pathMetadata.ino + ) { + await handle.close(); + throw new ArtifactReaderFailure( + "integrity", + `ASAR unpacked entry changed before read: ${entry.path}`, + {}, + { + logicalPath: entry.path, + declaredSha256: entry.declaredSha256, + calculatedSha256: null, + unpacked: true, + }, + ); + } + return handle; + } catch (cause: unknown) { + if (cause instanceof ArtifactReaderFailure) throw cause; + if (entry.unpacked && isMissingFile(cause)) + throw new ArtifactReaderFailure( + "unavailable", + `ASAR unpacked entry bytes are unavailable: ${entry.path}`, + { cause }, + { + logicalPath: entry.path, + declaredSha256: entry.declaredSha256, + calculatedSha256: null, + unpacked: true, + }, + ); + throw asarFailure(this.path, `read ${entry.path}`, cause); + } + } + + #resetArchiveState(): void { + this.#archiveSize = undefined; + this.#headerSize = undefined; + } } +type AsarFileMetadata = Extract, { size: number }>; +type AsarEntryState = { + readonly metadata: AsarFileMetadata; + readonly entry: ArtifactEntry; +}; +type UnpackedFileHandle = { + stat(): Promise; + close(): Promise; + createReadStream: FileHandle["createReadStream"]; +}; +type OpenAsarUnpackedFile = ( + path: string, + flags: number, +) => Promise; + +const copyArtifactEntry = (entry: ArtifactEntry): ArtifactEntry => ({ + ...entry, + limitations: [...entry.limitations], + ...(entry.sourceIdentity === undefined + ? {} + : { sourceIdentity: { ...entry.sourceIdentity } }), +}); + +const sameArtifactEntry = ( + entry: ArtifactEntry, + produced: ArtifactEntry, +): boolean => + entry.path === produced.path && + entry.kind === produced.kind && + entry.declaredSize === produced.declaredSize && + entry.compressedSize === produced.compressedSize && + entry.executable === produced.executable && + entry.encrypted === produced.encrypted && + entry.byteOffset === produced.byteOffset && + entry.declaredSha256 === produced.declaredSha256 && + entry.unpacked === produced.unpacked && + entry.adapterKey === produced.adapterKey && + entry.limitations.length === produced.limitations.length && + entry.limitations.every( + (limitation, index) => limitation === produced.limitations[index], + ) && + entry.sourceIdentity?.device === produced.sourceIdentity?.device && + entry.sourceIdentity?.inode === produced.sourceIdentity?.inode; + +const isAsarFileMetadata = ( + metadata: ReturnType, +): metadata is AsarFileMetadata => + "size" in metadata && + typeof metadata.size === "number" && + !("files" in metadata) && + !("link" in metadata); + +const parseArchiveOffset = (value: string): number | undefined => { + if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) return undefined; + const offset = Number(value); + return Number.isSafeInteger(offset) ? offset : undefined; +}; + const abortIfNeeded = (signal?: AbortSignal): void => { if (signal?.aborted === true) throw new ArtifactReaderFailure("cancelled", "ASAR operation cancelled"); diff --git a/src/artifacts/AsarEntryStream.ts b/src/artifacts/AsarEntryStream.ts new file mode 100644 index 0000000000..b11b233d6a --- /dev/null +++ b/src/artifacts/AsarEntryStream.ts @@ -0,0 +1,111 @@ +import { Readable } from "node:stream"; + +import { ArtifactReaderFailure } from "./ArtifactReader.js"; + +/** Close an ASAR file handle before honoring cancellation of an empty member. */ +export const closeAsarHandle = async ( + close: () => Promise, + signal?: AbortSignal, +): Promise => { + await close(); + abortIfNeeded(signal); +}; + +/** Validate and contextualize bytes read from one ASAR member source. */ +export const readValidatedAsarEntry = ( + source: Readable, + expectedBytes: number | undefined, + entryPath: string, + archivePath: string, + signal?: AbortSignal, +): Readable => { + const output = Readable.from( + (async function* () { + let observedBytes = 0; + const iterator = source[Symbol.asyncIterator](); + try { + while (true) { + abortIfNeeded(signal); + const next = await iterator.next(); + if (next.done) break; + const chunk: unknown = next.value; + if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) + throw new ArtifactReaderFailure( + "format", + `ASAR entry did not return bytes: ${entryPath}`, + ); + observedBytes += Buffer.isBuffer(chunk) + ? chunk.length + : chunk.byteLength; + if (expectedBytes !== undefined && observedBytes > expectedBytes) + throw new ArtifactReaderFailure( + "integrity", + `ASAR entry exceeded its declared size: ${entryPath}`, + ); + yield chunk; + } + if (expectedBytes !== undefined && observedBytes !== expectedBytes) + throw new ArtifactReaderFailure( + "integrity", + `ASAR entry size disagrees with its header: ${entryPath}`, + ); + } catch (cause: unknown) { + throw asarFailure(archivePath, `read ${entryPath}`, cause); + } finally { + try { + await iterator.return?.(); + } catch { + // Preserve the read failure; source destruction below owns cleanup. + } + if (!source.destroyed) source.destroy(); + } + })(), + ); + const closeSource = (): void => { + signal?.removeEventListener("abort", onAbort); + if (!source.destroyed) source.destroy(); + }; + const onAbort = (): void => { + closeSource(); + output.destroy( + new ArtifactReaderFailure("cancelled", "ASAR operation cancelled"), + ); + }; + output.once("close", closeSource); + signal?.addEventListener("abort", onAbort, { once: true }); + if (signal?.aborted === true) onAbort(); + return output; +}; + +const abortIfNeeded = (signal?: AbortSignal): void => { + if (signal?.aborted === true) + throw new ArtifactReaderFailure("cancelled", "ASAR operation cancelled"); +}; + +const asarFailure = ( + path: string, + operation: string, + cause: unknown, +): ArtifactReaderFailure => { + if (cause instanceof ArtifactReaderFailure) return cause; + if (isFilesystemFailure(cause)) + return new ArtifactReaderFailure( + "io", + `Could not ${operation} ASAR at ${path}: ${cause.message}`, + { cause }, + ); + return new ArtifactReaderFailure( + "format", + `Malformed ASAR during ${operation}: ${path}`, + { cause }, + ); +}; + +const isFilesystemFailure = ( + cause: unknown, +): cause is NodeJS.ErrnoException & Error => + cause instanceof Error && + "errno" in cause && + typeof cause.errno === "number" && + "syscall" in cause && + typeof cause.syscall === "string"; diff --git a/src/artifacts/DirectoryArtifactReader.ts b/src/artifacts/DirectoryArtifactReader.ts index cf4205c9e0..1a6cf26574 100644 --- a/src/artifacts/DirectoryArtifactReader.ts +++ b/src/artifacts/DirectoryArtifactReader.ts @@ -13,34 +13,48 @@ import { export class DirectoryArtifactReader implements ArtifactReader { readonly format = "directory" as const; readonly #rootPromise: Promise; + readonly #rootPath: string; constructor(root: string) { + this.#rootPath = root; this.#rootPromise = realpath(root); } async *entries(signal?: AbortSignal): AsyncIterable { - const root = await this.#rootPromise; + const root = await this.#rootPromise.catch((cause: unknown) => { + throw directoryIoFailure("resolve artifact root", this.#rootPath, cause); + }); const pending = [root]; while (pending.length > 0) { abortIfNeeded(signal); const directory = pending.pop(); if (directory === undefined) break; - await assertContainedDirectory(root, directory); + await withDirectoryIoContext("inspect directory", directory, () => + assertContainedDirectory(root, directory), + ); const children: string[] = []; - const handle = await opendir(directory); + const handle = await opendir(directory).catch((cause: unknown) => { + throw directoryIoFailure("open directory", directory, cause); + }); try { for await (const child of handle) children.push(child.name); + } catch (cause: unknown) { + throw directoryIoFailure("read directory", directory, cause); } finally { // best-effort cleanup: directory-handle close must not mask traversal. await handle.close().catch(() => undefined); } - await assertContainedDirectory(root, directory); + await withDirectoryIoContext("inspect directory", directory, () => + assertContainedDirectory(root, directory), + ); children.sort((left, right) => left.localeCompare(right, "en")); const directories: string[] = []; for (const name of children) { abortIfNeeded(signal); const absolute = join(directory, name); - const metadata = await lstat(absolute); + const metadata = await lstat(absolute).catch((cause: unknown) => { + throw directoryIoFailure("inspect entry", absolute, cause); + }); const path = relative(root, absolute).split(sep).join("/"); const kind = metadata.isSymbolicLink() ? "symlink" @@ -86,9 +100,17 @@ export class DirectoryArtifactReader implements ArtifactReader { const handle = await open( entry.adapterKey, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, - ); + ).catch((cause: unknown) => { + throw directoryIoFailure("open entry", entry.adapterKey, cause); + }); try { - const observed = await handle.stat(); + const observed = await handle.stat().catch((cause: unknown) => { + throw directoryIoFailure( + "inspect opened entry", + entry.adapterKey, + cause, + ); + }); if ( !observed.isFile() || entry.sourceIdentity === undefined || @@ -117,6 +139,38 @@ export class DirectoryArtifactReader implements ArtifactReader { } } +const directoryIoFailure = ( + operation: string, + path: string, + cause: unknown, +): ArtifactReaderFailure => { + if (cause instanceof ArtifactReaderFailure) return cause; + const code = + typeof cause === "object" && + cause !== null && + "code" in cause && + typeof cause.code === "string" + ? ` (${cause.code})` + : ""; + return new ArtifactReaderFailure( + "io", + `Could not ${operation} at ${path}${code}`, + { cause }, + ); +}; + +const withDirectoryIoContext = async ( + operation: string, + path: string, + action: () => Promise, +): Promise => { + try { + return await action(); + } catch (cause: unknown) { + throw directoryIoFailure(operation, path, cause); + } +}; + const abortIfNeeded = (signal?: AbortSignal): void => { if (signal?.aborted === true) throw new ArtifactReaderFailure( diff --git a/src/artifacts/NativeDmgArtifactReader.ts b/src/artifacts/NativeDmgArtifactReader.ts index b5c6ee73a6..2c8caecc18 100644 --- a/src/artifacts/NativeDmgArtifactReader.ts +++ b/src/artifacts/NativeDmgArtifactReader.ts @@ -37,13 +37,18 @@ export interface NativeDmgHost { arguments_: readonly string[], signal?: AbortSignal, options?: { readonly timeoutMs: number }, - ): Promise<{ readonly stdout: string; readonly exitCode: number }>; + ): Promise<{ + readonly stdout: string; + readonly stderr?: string; + readonly exitCode: number; + readonly cause?: unknown; + }>; } const systemHost: NativeDmgHost = { async run(arguments_, signal, options) { try { - const { stdout } = await execFileOutput( + const { stdout, stderr } = await execFileOutput( "/usr/bin/hdiutil", [...arguments_], { @@ -51,13 +56,23 @@ const systemHost: NativeDmgHost = { ...(signal === undefined ? {} : { signal }), }, ); - return { stdout, exitCode: 0 }; + return { stdout, stderr, exitCode: 0 }; } catch (cause: unknown) { if (cause instanceof Error && cause.name === "AbortError") throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled"); - throw new ArtifactReaderFailure("format", "hdiutil rejected the DMG", { - cause, - }); + const exitCode = processExitCode(cause); + if (exitCode !== undefined) + return { + stdout: processOutput(cause, "stdout"), + stderr: processOutput(cause, "stderr"), + exitCode, + cause, + }; + throw new ArtifactReaderFailure( + commandFailureReason(cause, arguments_[0]), + `hdiutil ${arguments_[0] ?? "operation"} failed: ${describeCommandFailure(cause, arguments_)}`, + { cause }, + ); } }, }; @@ -250,15 +265,157 @@ const runChecked = async ( signal?: AbortSignal, options?: { readonly timeoutMs: number }, ): Promise<{ readonly stdout: string; readonly exitCode: 0 }> => { - const result = await host.run(arguments_, signal, options); - if (result.exitCode !== 0) + let result: Awaited>; + try { + result = await host.run(arguments_, signal, options); + } catch (cause: unknown) { + if (cause instanceof ArtifactReaderFailure) throw cause; + if (cause instanceof Error && cause.name === "AbortError") + throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled", { + cause, + }); throw new ArtifactReaderFailure( - "unavailable", - `hdiutil ${arguments_[0] ?? "operation"} failed with a non-zero exit code`, + commandFailureReason(cause, arguments_[0]), + `hdiutil ${arguments_[0] ?? "operation"} failed: ${describeCommandFailure( + cause, + arguments_, + )}`, + { cause }, ); + } + if (result.exitCode !== 0) { + const failure = { + command: "/usr/bin/hdiutil", + exitCode: result.exitCode, + ...(result.cause === undefined + ? {} + : { code: processExitCode(result.cause) }), + stdout: result.stdout, + ...(result.stderr === undefined ? {} : { stderr: result.stderr }), + }; + const reason = commandFailureReason(failure, arguments_[0]); + const details = describeCommandFailure(failure, arguments_); + const unknownVerifyFailure = + arguments_[0] === "verify" && reason === "unavailable"; + throw new ArtifactReaderFailure( + reason, + unknownVerifyFailure + ? `hdiutil verify failed; captured diagnostics do not establish the failure cause: ${details}` + : `hdiutil ${arguments_[0] ?? "operation"} failed: ${details}`, + result.cause === undefined ? undefined : { cause: result.cause }, + ); + } return { stdout: result.stdout, exitCode: 0 }; }; +const describeCommandFailure = ( + cause: unknown, + arguments_: readonly string[], +): string => { + const fields: Record = { + command: "/usr/bin/hdiutil", + arguments: [...arguments_], + }; + if (typeof cause === "object" && cause !== null) { + for (const key of [ + "code", + "exitCode", + "errno", + "syscall", + "signal", + "stdout", + "stderr", + ] as const) { + const value = Reflect.get(cause, key); + if (typeof value === "string" || typeof value === "number") + fields[key] = value; + } + const code = Reflect.get(cause, "code"); + if (typeof code === "number") fields.exitCode = code; + } + if (cause instanceof ArtifactReaderFailure) fields.message = cause.message; + else if (cause instanceof Error) fields.message = cause.message; + return JSON.stringify(fields); +}; + +const processExitCode = (cause: unknown): number | undefined => { + if (typeof cause !== "object" || cause === null) return undefined; + const exitCode = Reflect.get(cause, "exitCode"); + if (typeof exitCode === "number") return exitCode; + const code = Reflect.get(cause, "code"); + return typeof code === "number" ? code : undefined; +}; + +const processOutput = (cause: unknown, field: "stdout" | "stderr"): string => { + if (typeof cause !== "object" || cause === null) return ""; + const value = Reflect.get(cause, field); + return typeof value === "string" ? value : ""; +}; + +const commandFailureReason = ( + cause: unknown, + operation: string | undefined, +): ArtifactReaderFailure["reason"] => { + if (typeof cause !== "object" || cause === null) return "unavailable"; + const code = Reflect.get(cause, "code"); + const exitCode = Reflect.get(cause, "exitCode"); + if (typeof code === "number" || typeof exitCode === "number") + return operation === "verify" ? verifyFailureReason(cause) : "unavailable"; + if (code === "ENOENT") { + const syscall = Reflect.get(cause, "syscall"); + return typeof syscall === "string" && syscall.startsWith("spawn") + ? "unavailable" + : "io"; + } + if ( + code === "EACCES" || + code === "EPERM" || + code === "EIO" || + code === "ENOTDIR" || + code === "EISDIR" || + code === "ENODEV" || + code === "EROFS" || + code === "EMFILE" || + code === "ENFILE" + ) + return "io"; + return "unavailable"; +}; + +const verifyFailureReason = ( + cause: unknown, +): ArtifactReaderFailure["reason"] => { + const diagnostic = verifyFailureDiagnostic( + processOutput(cause, "stdout"), + processOutput(cause, "stderr"), + ); + switch (diagnostic) { + case "image not recognized": + return "format"; + case "invalid checksum": + case "image data corrupted": + return "integrity"; + case "No such file or directory": + case "Permission denied": + case "Input/output error": + return "io"; + default: + return "unavailable"; + } +}; + +const verifyFailureDiagnostic = ( + stdout: string, + stderr: string, +): string | undefined => { + const prefix = "hdiutil: verify failed - "; + return `${stdout}\n${stderr}` + .split(/\r?\n/u) + .map((line) => line.trim()) + .find((line) => line.startsWith(prefix)) + ?.slice(prefix.length); +}; + const command = ( arguments_: readonly string[], effects: ArtifactCommand["effects"], diff --git a/src/artifacts/ZipArtifactReader.ts b/src/artifacts/ZipArtifactReader.ts index f71a3b35bc..8dd4471bde 100644 --- a/src/artifacts/ZipArtifactReader.ts +++ b/src/artifacts/ZipArtifactReader.ts @@ -53,6 +53,9 @@ export class ZipArtifactReader implements ArtifactReader { this.#reader = new ZipReader(this.#source, { checkSignature: true, checkOverlappingEntry: true, + // REA applies its own provider-neutral path validation in scanReader and + // must preserve that typed failure instead of zip.js rejecting first. + filenameValidation: "tolerant", }); } diff --git a/src/artifacts/apple/AppleAssetCatalogAnalysis.test.ts b/src/artifacts/apple/AppleAssetCatalogAnalysis.test.ts index ca0c036e6b..cda0cb5fcd 100644 --- a/src/artifacts/apple/AppleAssetCatalogAnalysis.test.ts +++ b/src/artifacts/apple/AppleAssetCatalogAnalysis.test.ts @@ -1,6 +1,14 @@ +import { mkdir, symlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + import { describe, expect, it } from "vitest"; -import { collectInterfaceBuilderResourceKeys } from "./AppleAssetCatalogAnalysis.js"; +import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; +import { ArtifactReaderFailure } from "../ArtifactReader.js"; +import { + analyzeAppleAssetCatalogs, + collectInterfaceBuilderResourceKeys, +} from "./AppleAssetCatalogAnalysis.js"; describe("Apple asset catalog application workflow", () => { it("joins only explicit resource-key fields with archive provenance", () => { @@ -40,4 +48,70 @@ describe("Apple asset catalog application workflow", () => { }, ]); }); + + it("reports an app without compiled catalogs as an empty observed inventory", async () => { + const bundle = await createTestTempDirectory("rea-asset-catalog-absent-"); + await mkdir(join(bundle, "Contents", "Resources"), { recursive: true }); + await writeFile(join(bundle, "Contents", "Resources", "icon.icns"), ""); + const result = await analyzeAppleAssetCatalogs({ + bundlePath: bundle, + targetSha256: "a".repeat(64), + runAssetUtil: () => { + throw new Error("assetutil must not run without a catalog"); + }, + }); + expect(result).toMatchObject({ + catalogs: [], + total_records: 0, + records: [], + resource_key_matches: [], + next_offset: null, + truncated: false, + }); + expect(result.limitations).toContain( + "No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.", + ); + }); + + it("does not report absence when the only catalog entry is a symlink", async () => { + const bundle = await createTestTempDirectory("rea-asset-catalog-link-"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(resources, { recursive: true }); + await writeFile(join(bundle, "outside.car"), "catalog"); + await symlink("../../outside.car", join(resources, "Assets.car")); + const rejected = await analyzeAppleAssetCatalogs({ + bundlePath: bundle, + targetSha256: "a".repeat(64), + runAssetUtil: () => Promise.resolve("[]"), + }).catch((cause: unknown) => cause); + expect(rejected).toBeInstanceOf(ArtifactReaderFailure); + expect(rejected).toMatchObject({ + reason: "path", + message: expect.stringContaining( + "Contents/Resources/Assets.car (symlink)", + ), + }); + }); + + it("discloses skipped catalog entries beside inspected catalogs", async () => { + const bundle = await createTestTempDirectory("rea-asset-catalog-mixed-"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(join(resources, "Nested"), { recursive: true }); + await writeFile(join(resources, "Assets.car"), "catalog"); + await symlink("../Assets.car", join(resources, "Nested", "Assets.car")); + const result = await analyzeAppleAssetCatalogs({ + bundlePath: bundle, + targetSha256: "a".repeat(64), + runAssetUtil: () => Promise.resolve("[]"), + }); + expect(result.catalogs.map(({ path }) => path)).toEqual([ + "Contents/Resources/Assets.car", + ]); + expect(result.limitations).toContainEqual( + expect.stringContaining("Contents/Resources/Nested/Assets.car (symlink)"), + ); + expect(result.limitations).not.toContainEqual( + expect.stringContaining("No compiled Assets.car catalog"), + ); + }); }); diff --git a/src/artifacts/apple/AppleAssetCatalogAnalysis.ts b/src/artifacts/apple/AppleAssetCatalogAnalysis.ts index 36b88eb134..65de9ab257 100644 --- a/src/artifacts/apple/AppleAssetCatalogAnalysis.ts +++ b/src/artifacts/apple/AppleAssetCatalogAnalysis.ts @@ -48,10 +48,15 @@ export const analyzeAppleAssetCatalogs = async (input: { sha256: string; records: ReturnType; }[] = []; + const uninspected: string[] = []; let totalMetadataBytes = 0; try { for await (const entry of reader.entries(input.signal)) { - if (entry.kind !== "file" || !entry.path.endsWith("Assets.car")) continue; + if (!entry.path.endsWith("Assets.car")) continue; + if (entry.kind !== "file") { + uninspected.push(`${entry.path} (${entry.kind})`); + continue; + } const inspected = await inspectAssetCatalogEntry({ reader, entry, @@ -69,10 +74,12 @@ export const analyzeAppleAssetCatalogs = async (input: { } finally { await reader.close(); } - if (catalogs.length === 0) + // Without any readable catalog, unmatched resource keys would overstate an + // absence that a skipped catalog entry may contradict. + if (catalogs.length === 0 && uninspected.length > 0) throw new ArtifactReaderFailure( - "unavailable", - "No compiled Assets.car catalogs were found in the app bundle", + "path", + `Asset catalog entries are not regular files and were not inspected: ${uninspected.join(", ")}`, ); const interfaceBuilder = await analyzeInterfaceBuilderBundle({ bundlePath: input.bundlePath, @@ -93,6 +100,18 @@ export const analyzeAppleAssetCatalogs = async (input: { ...result, limitations: [ ...result.limitations, + // An app without a compiled catalog is an observed empty inventory, not + // a missing host tool. + ...(catalogs.length === 0 + ? [ + "No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.", + ] + : []), + ...(uninspected.length === 0 + ? [] + : [ + `Asset catalog entries that are not regular files were not inspected, so resource keys reported as unmatched may resolve there: ${uninspected.join(", ")}`, + ]), "Exact resource-key joins use only recognized image or asset key fields on decoded Interface Builder resource objects; other archive resource references are not inferred.", ...(interfaceBuilder.graph.truncated ? [ diff --git a/src/artifacts/apple/DylibResolutionReader.ts b/src/artifacts/apple/DylibResolutionReader.ts new file mode 100644 index 0000000000..812e147150 --- /dev/null +++ b/src/artifacts/apple/DylibResolutionReader.ts @@ -0,0 +1,360 @@ +import { createHash } from "node:crypto"; +import { constants } from "node:fs"; +import { lstat, open, readlink, realpath } from "node:fs/promises"; +import { join, relative, sep } from "node:path"; + +import { resolveTreePath } from "../../domain/apple/dyldPaths.js"; +import { + dylibResolutionInputSchema, + dylibResolutionResultSchema, + traceDylibLoading, + type DylibResolutionResult, + type DylibTreeEntry, + type DylibTreeView, + type MachoImageFacts, +} from "../../domain/apple/dylibResolution.js"; +import { ArtifactReaderFailure } from "../ArtifactReader.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; +import { hasMachoMagic, readMachoImage } from "./MachoLoadCommandReader.js"; + +const HASH_CHUNK_BYTES = 1024 * 1024; + +/** Device, inode, size and change times of the file whose bytes were parsed. */ +type FileIdentity = string; + +/** Lazily probed, symlink-preserving view of one analyzed directory. */ +class FilesystemTreeView implements DylibTreeView { + readonly #entries = new Map>(); + readonly #images = new Map>(); + readonly #identities = new Map(); + + constructor( + private readonly root: string, + private readonly signal?: AbortSignal, + ) {} + + entry(path: string): Promise { + const cached = this.#entries.get(path); + if (cached !== undefined) return cached; + const pending = this.#readEntry(path); + this.#entries.set(path, pending); + return pending; + } + + image(path: string): Promise { + const cached = this.#images.get(path); + if (cached !== undefined) return cached; + const pending = readImage(join(this.root, path), this.signal).then( + ({ facts, identity }) => { + this.#identities.set(path, identity); + return facts; + }, + ); + this.#images.set(path, pending); + return pending; + } + + /** Identity of the file whose header bytes `image()` parsed. */ + identity(path: string): FileIdentity | undefined { + return this.#identities.get(path); + } + + async #readEntry(path: string): Promise { + cancelled(this.signal); + const absolute = join(this.root, path); + try { + const metadata = await lstat(absolute); + if (metadata.isSymbolicLink()) + return { kind: "symlink", target: await readlink(absolute) }; + if (metadata.isDirectory()) return { kind: "directory" }; + return metadata.isFile() ? { kind: "file" } : undefined; + } catch (cause: unknown) { + if (missing(cause)) return undefined; + throw cause; + } + } +} + +/** Resolve dyld load paths for the active Mach-O or every executable in its bundle. */ +export const traceDylibResolution = async (options: { + readonly rootPath: string; + readonly targetPath: string; + readonly targetSha256: string; + readonly enumerateRoots: boolean; + readonly parameters: unknown; + readonly signal?: AbortSignal; +}): Promise => { + const parsed = dylibResolutionInputSchema.safeParse(options.parameters); + if (!parsed.success) + throw new ArtifactReaderFailure( + "path", + "roots must be normalized paths relative to the analyzed root", + ); + // Canonicalization is inside the translated region: a revoked permission + // keeps its reason and path. + let root = options.rootPath; + try { + root = await realpath(options.rootPath); + const target = relative(root, await realpath(options.targetPath)) + .split(sep) + .join("/"); + const view = new FilesystemTreeView(root, options.signal); + const { roots: requested, unclassified } = + parsed.data.roots !== undefined + ? { roots: parsed.data.roots, unclassified: [] } + : options.enumerateRoots + ? await executableRoots(root, view, options.signal) + : { roots: [target], unclassified: [] }; + const roots = await requireMachoRoots(view, requested); + const trace = await traceDylibLoading(view, { + roots, + unclassified, + ...(parsed.data.architecture === undefined + ? {} + : { architecture: parsed.data.architecture }), + ...(options.signal === undefined ? {} : { signal: options.signal }), + }); + // Each digest must describe the same file whose headers were parsed. + const digests = new Map(); + for (const { path } of trace.images) + digests.set( + path, + await fileSha256(join(root, path), { + path, + expected: view.identity(path), + ...(options.signal === undefined ? {} : { signal: options.signal }), + }), + ); + const targetDigest = + digests.get(target) ?? + (await fileSha256(join(root, target), { + path: target, + expected: undefined, + ...(options.signal === undefined ? {} : { signal: options.signal }), + })); + if (targetDigest !== options.targetSha256) + throw new ArtifactReaderFailure( + "integrity", + `Active target digest changed: expected ${options.targetSha256}, observed ${targetDigest}`, + ); + return dylibResolutionResultSchema.parse({ + ...trace, + root_path: options.rootPath, + target_sha256: options.targetSha256, + images: trace.images.map((image) => ({ + ...image, + sha256: digests.get(image.path), + })), + }); + } catch (cause: unknown) { + if (options.signal?.aborted === true) + throw new ArtifactReaderFailure( + "cancelled", + "Dylib resolution was cancelled", + { cause }, + ); + const denied = permissionDenied(cause); + if (denied !== undefined) + throw new ArtifactReaderFailure( + "unavailable", + `Permission denied (${denied.code}) reading ${deniedPath(root, denied.path)}`, + { cause }, + ); + throw cause; + } +}; + +/** + * Every Mach-O in the bundle with an executable slice is its own process root. + * Mach-O files that do not parse are returned separately: whether they are + * executables is unknown, so they make coverage partial instead of vanishing. + */ +const executableRoots = async ( + root: string, + view: FilesystemTreeView, + signal?: AbortSignal, +): Promise<{ readonly roots: string[]; readonly unclassified: string[] }> => { + const roots: string[] = []; + const unclassified: string[] = []; + for await (const entry of new DirectoryArtifactReader(root).entries(signal)) { + if ( + entry.kind !== "file" || + !(await startsWithMachoMagic(entry.adapterKey)) + ) + continue; + const facts = await view.image(entry.path); + if (facts.status === "malformed" || facts.status === "unsupported") + unclassified.push(entry.path); + else if ( + facts.status === "parsed" && + facts.slices.some(({ file_type: type }) => type === "execute") + ) + roots.push(entry.path); + } + return { + roots: roots.sort(compare), + unclassified: unclassified.sort(compare), + }; +}; + +const compare = (left: string, right: string): number => + left < right ? -1 : left > right ? 1 : 0; + +/** + * Resolve each root segment by segment, as dependency candidates are, so a + * symlink in an intermediate directory cannot lead outside the analyzed root. + */ +const requireMachoRoots = async ( + view: FilesystemTreeView, + roots: readonly string[], +): Promise => { + const resolved: string[] = []; + for (const path of roots) { + const lookup = await resolveTreePath(view, path); + if (lookup.kind === "escapes") + throw new ArtifactReaderFailure( + "path", + `Root ${path} resolves outside the analyzed root`, + ); + if (lookup.kind !== "file") + throw new ArtifactReaderFailure( + "path", + `Root ${path} is not a regular file in the analyzed root`, + ); + if ((await view.image(lookup.path)).status === "not-mach-o") + throw new ArtifactReaderFailure( + "format", + `Root ${path} is not a Mach-O image`, + ); + if (!resolved.includes(lookup.path)) resolved.push(lookup.path); + } + return resolved; +}; + +const startsWithMachoMagic = async (path: string): Promise => { + const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + const buffer = new Uint8Array(4); + const { bytesRead } = await handle.read(buffer, 0, 4, 0); + return hasMachoMagic(buffer.subarray(0, bytesRead)); + } finally { + await handle.close(); + } +}; + +const identityOf = (metadata: { + readonly dev: bigint; + readonly ino: bigint; + readonly size: bigint; + readonly mtimeNs: bigint; + readonly ctimeNs: bigint; +}): FileIdentity => + [ + metadata.dev, + metadata.ino, + metadata.size, + metadata.mtimeNs, + metadata.ctimeNs, + ].join(":"); + +const readImage = async ( + path: string, + signal?: AbortSignal, +): Promise<{ + readonly facts: MachoImageFacts; + readonly identity: FileIdentity; +}> => { + cancelled(signal); + const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + const metadata = await handle.stat({ bigint: true }); + const size = Number(metadata.size); + const facts = await readMachoImage(async (offset, length) => { + cancelled(signal); + const buffer = new Uint8Array( + Math.max(0, Math.min(length, size - offset)), + ); + const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset); + return buffer.subarray(0, bytesRead); + }, size); + return { facts, identity: identityOf(metadata) }; + } finally { + await handle.close(); + } +}; + +/** Hash a file, failing if it is not the file (or the version) that was parsed. */ +const fileSha256 = async ( + absolute: string, + options: { + readonly path: string; + readonly expected: FileIdentity | undefined; + readonly signal?: AbortSignal; + }, +): Promise => { + const { signal } = options; + const handle = await open( + absolute, + constants.O_RDONLY | constants.O_NOFOLLOW, + ); + const unchanged = async (): Promise => { + if ( + options.expected !== undefined && + identityOf(await handle.stat({ bigint: true })) !== options.expected + ) + throw new ArtifactReaderFailure( + "integrity", + `${options.path} changed while its dyld load commands were traced`, + ); + }; + try { + await unchanged(); + const hash = createHash("sha256"); + const buffer = new Uint8Array(HASH_CHUNK_BYTES); + for (;;) { + cancelled(signal); + const { bytesRead } = await handle.read(buffer, 0, buffer.length, null); + if (bytesRead === 0) break; + hash.update(buffer.subarray(0, bytesRead)); + } + await unchanged(); + return hash.digest("hex"); + } finally { + await handle.close(); + } +}; + +const cancelled = (signal?: AbortSignal): void => { + if (signal?.aborted === true) + throw new ArtifactReaderFailure( + "cancelled", + "Dylib resolution was cancelled", + ); +}; + +/** A denied path relative to the analyzed root, or absolute when outside it. */ +const deniedPath = (root: string, path: string | undefined): string => { + if (path === undefined) return "a file in the analyzed root"; + const inside = relative(root, path); + return inside === "" || inside.startsWith("..") ? path : inside; +}; + +/** Host permission denials, kept distinct from malformed or missing files. */ +const permissionDenied = ( + cause: unknown, +): { readonly code: string; readonly path: string | undefined } | undefined => { + if (!(cause instanceof Error) || !("code" in cause)) return undefined; + if (cause.code !== "EACCES" && cause.code !== "EPERM") return undefined; + return { + code: cause.code, + path: + "path" in cause && typeof cause.path === "string" + ? cause.path + : undefined, + }; +}; + +const missing = (cause: unknown): boolean => + cause instanceof Error && + "code" in cause && + (cause.code === "ENOENT" || cause.code === "ENOTDIR"); diff --git a/src/artifacts/apple/KeyedArchiveReader.test.ts b/src/artifacts/apple/KeyedArchiveReader.test.ts index 870dc55e7b..3910eef5cf 100644 --- a/src/artifacts/apple/KeyedArchiveReader.test.ts +++ b/src/artifacts/apple/KeyedArchiveReader.test.ts @@ -125,6 +125,16 @@ describe("inert keyed archive decoding", () => { offset: 0, limit: 1, }), - ).toThrow("does not exist"); + ).toThrow( + expect.objectContaining({ + issues: [ + expect.objectContaining({ + path: ["root"], + message: expect.stringContaining("does not exist"), + expected: ["root", "other"], + }), + ], + }), + ); }); }); diff --git a/src/artifacts/apple/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts index e948619971..fafa70fbe8 100644 --- a/src/artifacts/apple/KeyedArchiveReader.ts +++ b/src/artifacts/apple/KeyedArchiveReader.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { TextDecoder } from "node:util"; import { parseBinary } from "plist"; import { z } from "zod"; +import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import type { JsonValue } from "../../domain/jsonValue.js"; import { projectPlistValue } from "../../domain/apple/plistValue.js"; import { @@ -25,6 +26,10 @@ export const decodeKeyedArchiveBytes = ( ) => { if (bytes.length > MAX_BYTES) throw new RangeError("Keyed archive exceeds 64 MiB"); + if (bytes.subarray(0, 10).toString("ascii") === "NIBArchive") + throw new TypeError( + "Selected file is a compiled NIBArchive, not a Foundation plist archive; decode it with decode_interface_builder", + ); const binary = bytes.subarray(0, 8).toString("ascii") === "bplist00"; const parsed = binary ? { value: parseBinary(bytes), omittedPrototypeKeys: 0 } @@ -83,18 +88,20 @@ export const inspectBundleKeyedArchive = async (input: { .split("/") .some((part) => part === ".." || part === "." || part.length === 0) ) - throw new ArtifactReaderFailure( - "path", - "Archive path must be a canonical relative bundle path", + throw archivePathError( + "invalid_format", + selected.path === "." + ? "Select a keyed archive path relative to the active app bundle." + : `Archive path must be a canonical relative bundle path without empty, '.', or '..' segments: ${selected.path}`, ); const reader = new DirectoryArtifactReader(input.bundlePath); try { for await (const entry of reader.entries(input.signal)) { if (entry.path !== selected.path) continue; if (entry.kind !== "file") - throw new ArtifactReaderFailure( - "path", - "Archive must be a regular file, not a symlink or directory", + throw archivePathError( + "invalid_value", + `Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`, ); if ((entry.declaredSize ?? 0) > MAX_BYTES) throw new ArtifactReaderFailure( @@ -132,18 +139,28 @@ export const inspectBundleKeyedArchive = async (input: { ...decodeKeyedArchiveBytes(bytes, selected), }); } catch (cause) { + if (cause instanceof AnalysisInputError) throw cause; throw new ArtifactReaderFailure( cause instanceof RangeError ? "limit" : "format", - "Cannot decode selected Foundation keyed archive", + `Cannot decode selected Foundation keyed archive: ${cause instanceof Error ? cause.message : String(cause)}`, { cause }, ); } } - throw new ArtifactReaderFailure( - "path", - `Archive not found in bundle: ${selected.path}`, + throw archivePathError( + "invalid_value", + `No regular file exists at ${selected.path} in the active app bundle.`, ); } finally { await reader.close(); } }; + +/** A caller-selected archive path, not the artifact, failed its constraint. */ +const archivePathError = ( + reason: "invalid_format" | "invalid_value", + message: string, +) => + new AnalysisInputError("inspect_keyed_archive", undefined, [ + { path: ["path"], reason, message }, + ]); diff --git a/src/artifacts/apple/MachoImage.fixture.ts b/src/artifacts/apple/MachoImage.fixture.ts new file mode 100644 index 0000000000..634ab4ecf6 --- /dev/null +++ b/src/artifacts/apple/MachoImage.fixture.ts @@ -0,0 +1,152 @@ +/** Little-endian Mach-O images and FAT containers built field by field for tests. */ + +export const LC = { + LOAD_DYLIB: 0xc, + ID_DYLIB: 0xd, + LOAD_WEAK_DYLIB: 0x80000018, + REEXPORT_DYLIB: 0x8000001f, + LAZY_LOAD_DYLIB: 0x20, + LOAD_UPWARD_DYLIB: 0x80000023, + RPATH: 0x8000001c, + DYLD_ENVIRONMENT: 0x27, + CODE_SIGNATURE: 0x1d, +} as const; + +export const CPU = { + arm64: { type: 0x0100000c, subtype: 0 }, + arm64e: { type: 0x0100000c, subtype: 0x80000002 }, + x86_64: { type: 0x01000007, subtype: 3 }, +} as const; + +export const FILE_TYPE = { execute: 2, dylib: 6, bundle: 8 } as const; + +const encoder = new TextEncoder(); + +/** One load command with a NUL-terminated string at `nameOffset`, padded to 8 bytes. */ +const commandWithString = ( + command: number, + fields: readonly number[], + text: string, +): Uint8Array => { + const nameOffset = 8 + fields.length * 4 + 4; + const name = encoder.encode(text); + const size = Math.ceil((nameOffset + name.length + 1) / 8) * 8; + const bytes = new Uint8Array(size); + const view = new DataView(bytes.buffer); + view.setUint32(0, command, true); + view.setUint32(4, size, true); + view.setUint32(8, nameOffset, true); + fields.forEach((value, index) => view.setUint32(12 + index * 4, value, true)); + bytes.set(name, nameOffset); + return bytes; +}; + +/** Classic `dylib_command`: timestamp, current and compatibility versions. */ +export const dylibCommand = ( + command: number, + name: string, + versions: { readonly current?: number; readonly compatibility?: number } = {}, +): Uint8Array => + commandWithString( + command, + [2, versions.current ?? 0x10000, versions.compatibility ?? 0x10000], + name, + ); + +/** `dylib_use_command`: marker, versions and `DYLIB_USE_*` flags. */ +export const dylibUseCommand = ( + command: number, + name: string, + flags: number, +): Uint8Array => + commandWithString(command, [0x1a741800, 0x10000, 0x10000, flags], name); + +export const rpathCommand = (path: string): Uint8Array => + commandWithString(LC.RPATH, [], path); + +export const dyldEnvironmentCommand = (value: string): Uint8Array => + commandWithString(LC.DYLD_ENVIRONMENT, [], value); + +export const codeSignatureCommand = (): Uint8Array => { + const bytes = new Uint8Array(16); + const view = new DataView(bytes.buffer); + view.setUint32(0, LC.CODE_SIGNATURE, true); + view.setUint32(4, 16, true); + return bytes; +}; + +/** A 64-bit (or 32-bit) little-endian Mach-O image made of the given commands. */ +export const machoImage = (options: { + readonly cpu?: { readonly type: number; readonly subtype: number }; + readonly fileType?: number; + readonly commands?: readonly Uint8Array[]; + readonly wide?: boolean; +}): Uint8Array => { + const wide = options.wide ?? true; + const commands = options.commands ?? []; + const headerSize = wide ? 32 : 28; + const commandBytes = commands.reduce((total, item) => total + item.length, 0); + const bytes = new Uint8Array(headerSize + commandBytes + 16); + const view = new DataView(bytes.buffer); + const cpu = options.cpu ?? CPU.arm64; + view.setUint32(0, wide ? 0xfeedfacf : 0xfeedface, true); + view.setUint32(4, cpu.type, true); + view.setUint32(8, cpu.subtype, true); + view.setUint32(12, options.fileType ?? FILE_TYPE.execute, true); + view.setUint32(16, commands.length, true); + view.setUint32(20, commandBytes, true); + let offset = headerSize; + for (const command of commands) { + bytes.set(command, offset); + offset += command.length; + } + return bytes; +}; + +/** + * A FAT container whose slices start on 4 KiB boundaries. Headers are + * big-endian unless `littleEndian` selects the swapped FAT_CIGAM magics. + */ +export const fatImage = ( + slices: readonly { + readonly cpu: { readonly type: number; readonly subtype: number }; + readonly bytes: Uint8Array; + }[], + wide = false, + littleEndian = false, +): Uint8Array => { + const entrySize = wide ? 32 : 20; + const offsets: number[] = []; + let end = 4096; + for (const slice of slices) { + offsets.push(end); + end += Math.ceil(slice.bytes.length / 4096) * 4096; + } + const bytes = new Uint8Array(end); + const view = new DataView(bytes.buffer); + view.setUint32(0, wide ? 0xcafebabf : 0xcafebabe, littleEndian); + view.setUint32(4, slices.length, littleEndian); + slices.forEach((slice, index) => { + const base = 8 + index * entrySize; + const offset = offsets[index] ?? 0; + view.setUint32(base, slice.cpu.type, littleEndian); + view.setUint32(base + 4, slice.cpu.subtype, littleEndian); + if (wide) { + view.setBigUint64(base + 8, BigInt(offset), littleEndian); + view.setBigUint64(base + 16, BigInt(slice.bytes.length), littleEndian); + view.setUint32(base + 24, 12, littleEndian); + } else { + view.setUint32(base + 8, offset, littleEndian); + view.setUint32(base + 12, slice.bytes.length, littleEndian); + view.setUint32(base + 16, 12, littleEndian); + } + bytes.set(slice.bytes, offset); + }); + return bytes; +}; + +/** In-memory `ReadAt` over one buffer. */ +export const readerOf = + (bytes: Uint8Array) => + (offset: number, length: number): Promise => + Promise.resolve(bytes.subarray(offset, offset + length)); diff --git a/src/artifacts/apple/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts new file mode 100644 index 0000000000..892312ea0d --- /dev/null +++ b/src/artifacts/apple/MachoLoadCommandReader.test.ts @@ -0,0 +1,320 @@ +import { describe, expect, it } from "vitest"; + +import { + CPU, + FILE_TYPE, + LC, + codeSignatureCommand, + dyldEnvironmentCommand, + dylibCommand, + dylibUseCommand, + fatImage, + machoImage, + readerOf, + rpathCommand, +} from "./MachoImage.fixture.js"; +import { + MAX_LOAD_COMMAND_BYTES, + hasMachoMagic, + readMachoImage, +} from "./MachoLoadCommandReader.js"; + +const read = (bytes: Uint8Array) => + readMachoImage(readerOf(bytes), bytes.length); + +describe("Mach-O load command reader", () => { + it("decodes dylib loading commands of a thin image", async () => { + const image = machoImage({ + fileType: FILE_TYPE.dylib, + commands: [ + dylibCommand(LC.ID_DYLIB, "@rpath/Core.framework/Core"), + dylibCommand(LC.LOAD_DYLIB, "/usr/lib/libSystem.B.dylib", { + current: 0x050f0400, + compatibility: 0x10000, + }), + dylibCommand(LC.LOAD_WEAK_DYLIB, "@rpath/libweak.dylib"), + dylibCommand(LC.REEXPORT_DYLIB, "@loader_path/libre.dylib"), + dylibCommand(LC.LOAD_UPWARD_DYLIB, "@rpath/libup.dylib"), + dylibCommand(LC.LAZY_LOAD_DYLIB, "@rpath/liblazy.dylib"), + rpathCommand("@loader_path/Frameworks"), + rpathCommand("/usr/lib/swift"), + dyldEnvironmentCommand("DYLD_FRAMEWORK_PATH=@executable_path"), + codeSignatureCommand(), + ], + }); + const facts = await read(image); + expect(facts.status).toBe("parsed"); + if (facts.status !== "parsed") return; + expect(facts.slices).toHaveLength(1); + const [slice] = facts.slices; + expect(slice).toMatchObject({ + architecture: "arm64", + file_type: "dylib", + install_name: "@rpath/Core.framework/Core", + rpaths: ["@loader_path/Frameworks", "/usr/lib/swift"], + dyld_environment: ["DYLD_FRAMEWORK_PATH=@executable_path"], + code_signature_present: true, + }); + expect( + slice?.dependencies.map( + ({ command, install_name: name, weak, reexport, upward }) => [ + command, + name, + weak, + reexport, + upward, + ], + ), + ).toEqual([ + ["LC_LOAD_DYLIB", "/usr/lib/libSystem.B.dylib", false, false, false], + ["LC_LOAD_WEAK_DYLIB", "@rpath/libweak.dylib", true, false, false], + ["LC_REEXPORT_DYLIB", "@loader_path/libre.dylib", false, true, false], + ["LC_LOAD_UPWARD_DYLIB", "@rpath/libup.dylib", false, false, true], + ["LC_LAZY_LOAD_DYLIB", "@rpath/liblazy.dylib", false, false, false], + ]); + expect(slice?.dependencies[0]).toMatchObject({ + encoding: "dylib_command", + current_version: "1295.4.0", + compatibility_version: "1.0.0", + }); + }); + + it("decodes dylib_use_command flags", async () => { + const facts = await read( + machoImage({ + commands: [ + dylibUseCommand(LC.LOAD_DYLIB, "@rpath/libdelay.dylib", 0x8), + dylibUseCommand(LC.LOAD_DYLIB, "@rpath/libweakup.dylib", 0x1 | 0x4), + dylibUseCommand(LC.LOAD_DYLIB, "@rpath/libre.dylib", 0x2), + ], + }), + ); + if (facts.status !== "parsed") throw new Error(facts.status); + expect( + facts.slices[0]?.dependencies.map( + ({ encoding, weak, upward, reexport, delayed_init: delayed }) => ({ + encoding, + weak, + upward, + reexport, + delayed, + }), + ), + ).toEqual([ + { + encoding: "dylib_use_command", + weak: false, + upward: false, + reexport: false, + delayed: true, + }, + { + encoding: "dylib_use_command", + weak: true, + upward: true, + reexport: false, + delayed: false, + }, + { + encoding: "dylib_use_command", + weak: false, + upward: false, + reexport: true, + delayed: false, + }, + ]); + }); + + it("reads every slice of FAT and FAT_64 containers in either byte order", async () => { + for (const [wide, littleEndian] of [ + [false, false], + [true, false], + [false, true], + [true, true], + ] as const) { + const facts = await read( + fatImage( + [ + { + cpu: CPU.arm64e, + bytes: machoImage({ + cpu: CPU.arm64e, + commands: [rpathCommand("@executable_path/a")], + }), + }, + { + cpu: CPU.x86_64, + bytes: machoImage({ + cpu: CPU.x86_64, + commands: [rpathCommand("@executable_path/b")], + }), + }, + ], + wide, + littleEndian, + ), + ); + if (facts.status !== "parsed") throw new Error(facts.status); + expect( + facts.slices.map(({ architecture, rpaths }) => [architecture, rpaths]), + ).toEqual([ + ["arm64e", ["@executable_path/a"]], + ["x86_64", ["@executable_path/b"]], + ]); + } + }); +}); + +describe("Mach-O load command reader inputs", () => { + it("names 32-bit and non-Mach-O inputs without guessing", async () => { + const i386 = await read( + machoImage({ cpu: { type: 7, subtype: 3 }, wide: false }), + ); + expect(i386.status === "parsed" && i386.slices[0]?.architecture).toBe( + "i386", + ); + // A Java class file shares the FAT magic; its version is not a slice count. + const javaClass = Uint8Array.from([0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 0x34]); + expect(hasMachoMagic(javaClass)).toBe(true); + expect(await read(javaClass)).toEqual({ status: "not-mach-o" }); + expect(await read(new TextEncoder().encode("#!/bin/sh\n"))).toEqual({ + status: "not-mach-o", + }); + expect(await read(Uint8Array.from([0xcf, 0xfa]))).toEqual({ + status: "not-mach-o", + }); + // A recognized magic without room for a header is a truncated image. + expect( + await read(Uint8Array.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0x00])), + ).toEqual({ status: "malformed", reason: "Mach-O header is truncated" }); + }); +}); + +describe("Mach-O load command reader failures", () => { + const withCommandSize = (size: number): Uint8Array => { + const image = machoImage({ commands: [rpathCommand("@loader_path")] }); + new DataView(image.buffer).setUint32(32 + 4, size, true); + return image; + }; + + it("reports malformed command tables with the failing command", async () => { + expect(await read(withCommandSize(0))).toMatchObject({ + status: "malformed", + reason: expect.stringContaining("load command 0"), + }); + expect(await read(withCommandSize(4096))).toMatchObject({ + status: "malformed", + reason: expect.stringContaining("invalid cmdsize"), + }); + const extraCommand = machoImage({ commands: [rpathCommand("x")] }); + new DataView(extraCommand.buffer).setUint32(16, 2, true); + expect(await read(extraCommand)).toMatchObject({ + status: "malformed", + reason: "load command 1 starts beyond sizeofcmds", + }); + }); + + it("requires 8-byte command alignment in 64-bit images", async () => { + // A command the reader does not decode; only its size is checked. + const command = new Uint8Array(12); + const view = new DataView(command.buffer); + view.setUint32(0, 0x7f000001, true); + view.setUint32(4, 12, true); + expect(await read(machoImage({ commands: [command] }))).toMatchObject({ + status: "malformed", + reason: expect.stringContaining("invalid cmdsize 12"), + }); + expect( + await read( + machoImage({ + cpu: { type: 7, subtype: 3 }, + wide: false, + commands: [command], + }), + ), + ).toMatchObject({ status: "parsed" }); + }); + + it("accepts FAT tables up to 128 records that name a Mach-O CPU", async () => { + const slices = Array.from({ length: 24 }, () => ({ + cpu: CPU.arm64, + bytes: machoImage({}), + })); + const facts = await read(fatImage(slices)); + expect(facts.status === "parsed" && facts.slices).toHaveLength(24); + // An empty table is not a Java class file, whose major version is nonzero. + for (const magic of [ + [0xca, 0xfe, 0xba, 0xbe], + [0xbf, 0xba, 0xfe, 0xca], + ]) + expect(await read(Uint8Array.from([...magic, 0, 0, 0, 0]))).toEqual({ + status: "malformed", + reason: "FAT header declares no architectures", + }); + }); + + it("rejects string commands too short to hold their string offset", async () => { + const short = new Uint8Array(8); + const view = new DataView(short.buffer); + view.setUint32(0, LC.RPATH, true); + view.setUint32(4, 8, true); + expect(await read(machoImage({ commands: [short] }))).toMatchObject({ + status: "malformed", + reason: "load command 0 is too short to hold a string offset", + }); + view.setUint32(0, LC.CODE_SIGNATURE, true); + expect(await read(machoImage({ commands: [short] }))).toMatchObject({ + status: "malformed", + reason: "load command 0 is too short for LC_CODE_SIGNATURE", + }); + }); + + it("rejects strings that are unterminated, misplaced or not UTF-8", async () => { + const unterminated = rpathCommand("abcd"); + unterminated.fill(0x41, 12); + expect(await read(machoImage({ commands: [unterminated] }))).toMatchObject({ + status: "malformed", + reason: expect.stringContaining("unterminated"), + }); + const misplaced = rpathCommand("abcd"); + new DataView(misplaced.buffer).setUint32(8, 4, true); + expect(await read(machoImage({ commands: [misplaced] }))).toMatchObject({ + status: "malformed", + reason: expect.stringContaining("string offset 4"), + }); + const invalid = rpathCommand("ab"); + invalid[12] = 0xff; + expect(await read(machoImage({ commands: [invalid] }))).toMatchObject({ + status: "malformed", + reason: expect.stringContaining("not UTF-8"), + }); + }); + + it("refuses truncated, oversized, big-endian and out-of-range images", async () => { + const image = machoImage({ commands: [rpathCommand("@loader_path")] }); + expect(await read(image.subarray(0, 20))).toMatchObject({ + status: "malformed", + reason: "Mach-O header is truncated", + }); + const oversized = machoImage({}); + new DataView(oversized.buffer).setUint32( + 20, + MAX_LOAD_COMMAND_BYTES + 8, + true, + ); + expect(await read(oversized)).toMatchObject({ status: "unsupported" }); + const beyond = machoImage({ commands: [rpathCommand("x")] }); + new DataView(beyond.buffer).setUint32(20, beyond.length, true); + expect(await read(beyond)).toMatchObject({ + status: "malformed", + reason: "load commands extend beyond the Mach-O slice", + }); + const bigEndian = Uint8Array.from([0xfe, 0xed, 0xfa, 0xcf, 0, 0, 0, 0]); + expect(await read(bigEndian)).toMatchObject({ status: "unsupported" }); + const fat = fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }]); + expect(await read(fat.subarray(0, 4100))).toMatchObject({ + status: "malformed", + reason: "FAT architecture 0 extends beyond the file", + }); + }); +}); diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts new file mode 100644 index 0000000000..b78ce7fbc6 --- /dev/null +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -0,0 +1,426 @@ +import type { + MachoDependency, + MachoImageFacts, + MachoSlice, +} from "../../domain/apple/dylibResolution.js"; + +/** Read `length` bytes at `offset`; shorter results mean end of file. */ +export type ReadAt = (offset: number, length: number) => Promise; + +/** Load commands larger than this are refused instead of buffered. */ +export const MAX_LOAD_COMMAND_BYTES = 16 * 1024 * 1024; +/** Target opening accepts up to 128 FAT records; Java class files share the FAT magic. */ +const MAX_FAT_ARCHITECTURES = 128; +/** CPU types of real Mach-O slices (x86, x86_64, arm, arm64, arm64_32, ppc, ppc64). */ +const KNOWN_CPU_TYPES: readonly number[] = [ + 7, 0x01000007, 12, 0x0100000c, 0x0200000c, 18, 0x01000012, +]; + +const MH_MAGIC = 0xfeedface; +const MH_MAGIC_64 = 0xfeedfacf; +const MH_CIGAM = 0xcefaedfe; +const MH_CIGAM_64 = 0xcffaedfe; +const FAT_MAGIC = 0xcafebabe; +const FAT_MAGIC_64 = 0xcafebabf; +const FAT_CIGAM = 0xbebafeca; +const FAT_CIGAM_64 = 0xbfbafeca; + +const LC_ID_DYLIB = 0xd; +const LC_LOAD_DYLIB = 0xc; +const LC_LOAD_WEAK_DYLIB = 0x80000018; +const LC_REEXPORT_DYLIB = 0x8000001f; +const LC_LAZY_LOAD_DYLIB = 0x20; +const LC_LOAD_UPWARD_DYLIB = 0x80000023; +const LC_RPATH = 0x8000001c; +const LC_DYLD_ENVIRONMENT = 0x27; +const LC_CODE_SIGNATURE = 0x1d; + +/** `mach-o/loader.h` `dylib_use_command`: `nameoff == 28` and this marker. */ +const DYLIB_USE_MARKER = 0x1a741800; +const DYLIB_USE_WEAK_LINK = 0x1; +const DYLIB_USE_REEXPORT = 0x2; +const DYLIB_USE_UPWARD = 0x4; +const DYLIB_USE_DELAYED_INIT = 0x8; + +const DEPENDENCY_COMMANDS: ReadonlyMap = + new Map([ + [LC_LOAD_DYLIB, "LC_LOAD_DYLIB"], + [LC_LOAD_WEAK_DYLIB, "LC_LOAD_WEAK_DYLIB"], + [LC_REEXPORT_DYLIB, "LC_REEXPORT_DYLIB"], + [LC_LAZY_LOAD_DYLIB, "LC_LAZY_LOAD_DYLIB"], + [LC_LOAD_UPWARD_DYLIB, "LC_LOAD_UPWARD_DYLIB"], + ]); + +/** Malformed or unsupported structure found while decoding one image. */ +class MachoFormatIssue extends Error { + constructor( + readonly status: "malformed" | "unsupported", + message: string, + ) { + super(message); + } +} + +/** Report whether the first bytes carry a Mach-O or FAT magic number. */ +export const hasMachoMagic = (head: Uint8Array): boolean => { + if (head.byteLength < 4) return false; + const magic = new DataView( + head.buffer, + head.byteOffset, + head.byteLength, + ).getUint32(0, false); + return [ + MH_MAGIC, + MH_MAGIC_64, + MH_CIGAM, + MH_CIGAM_64, + FAT_MAGIC, + FAT_MAGIC_64, + FAT_CIGAM, + FAT_CIGAM_64, + ].includes(magic); +}; + +/** Decode every slice's dylib-loading load commands without reading code. */ +export const readMachoImage = async ( + readAt: ReadAt, + size: number, +): Promise => { + try { + const head = await readAt(0, Math.min(size, 8)); + if (head.byteLength < 8) + // A recognized magic with no room for a header is a truncated image. + return hasMachoMagic(head) + ? { status: "malformed", reason: "Mach-O header is truncated" } + : { status: "not-mach-o" }; + const view = viewOf(head); + const magic = view.getUint32(0, false); + if ([FAT_MAGIC, FAT_MAGIC_64, FAT_CIGAM, FAT_CIGAM_64].includes(magic)) { + // FAT headers are big-endian; the swapped magics store them little-endian. + const littleEndian = magic === FAT_CIGAM || magic === FAT_CIGAM_64; + const count = view.getUint32(4, littleEndian); + const wide = magic === FAT_MAGIC_64 || magic === FAT_CIGAM_64; + // A Java class file's nonzero major version reads as the count, so an + // empty architecture table can only be a malformed universal header. + if (count === 0) + return { + status: "malformed", + reason: "FAT header declares no architectures", + }; + if (count > MAX_FAT_ARCHITECTURES || 8 + count * (wide ? 32 : 20) > size) + return { status: "not-mach-o" }; + const table = viewOf( + await readExact(readAt, 8, count * (wide ? 32 : 20), "FAT table"), + ); + // A Java class file's version fields read as a FAT count; its "table" names no CPU. + const cpuTypes = Array.from({ length: count }, (_, index) => + table.getUint32(index * (wide ? 32 : 20), littleEndian), + ); + if (!cpuTypes.some((type) => KNOWN_CPU_TYPES.includes(type))) + return { status: "not-mach-o" }; + return { + status: "parsed", + slices: await readFatSlices(readAt, size, { + wide, + littleEndian, + count, + }), + }; + } + if (magic === MH_CIGAM || magic === MH_CIGAM_64) + return { status: "parsed", slices: [await readSlice(readAt, 0, size)] }; + if (magic === MH_MAGIC || magic === MH_MAGIC_64) + throw new MachoFormatIssue( + "unsupported", + "big-endian Mach-O images are not supported", + ); + return { status: "not-mach-o" }; + } catch (cause: unknown) { + if (cause instanceof MachoFormatIssue) + return { status: cause.status, reason: cause.message }; + throw cause; + } +}; + +const readFatSlices = async ( + readAt: ReadAt, + size: number, + table: { + readonly wide: boolean; + readonly littleEndian: boolean; + readonly count: number; + }, +): Promise => { + const { wide, littleEndian, count } = table; + const entrySize = wide ? 32 : 20; + const view = viewOf( + await readExact(readAt, 8, count * entrySize, "FAT table"), + ); + const slices: MachoSlice[] = []; + for (let index = 0; index < count; index++) { + const base = index * entrySize; + const offset = wide + ? safeNumber(view.getBigUint64(base + 8, littleEndian)) + : view.getUint32(base + 8, littleEndian); + const length = wide + ? safeNumber(view.getBigUint64(base + 16, littleEndian)) + : view.getUint32(base + 12, littleEndian); + if (offset + length > size) + throw new MachoFormatIssue( + "malformed", + `FAT architecture ${index} extends beyond the file`, + ); + slices.push(await readSlice(readAt, offset, length)); + } + return slices; +}; + +const readSlice = async ( + readAt: ReadAt, + base: number, + length: number, +): Promise => { + const header = viewOf(await readExact(readAt, base, 28, "Mach-O header")); + const magic = header.getUint32(0, true); + if (magic === MH_MAGIC_64 || magic === MH_MAGIC) { + const wide = magic === MH_MAGIC_64; + const headerSize = wide ? 32 : 28; + const commandCount = header.getUint32(16, true); + const commandBytes = header.getUint32(20, true); + if (commandBytes > MAX_LOAD_COMMAND_BYTES) + throw new MachoFormatIssue( + "unsupported", + `load commands occupy ${commandBytes} bytes, above the ${MAX_LOAD_COMMAND_BYTES}-byte limit`, + ); + if (headerSize + commandBytes > length) + throw new MachoFormatIssue( + "malformed", + "load commands extend beyond the Mach-O slice", + ); + const commands = await readExact( + readAt, + base + headerSize, + commandBytes, + "load commands", + ); + return decodeCommands(commands, { + wide, + cpuType: header.getUint32(4, true), + cpuSubtype: header.getUint32(8, true), + fileType: header.getUint32(12, true), + commandCount, + }); + } + throw new MachoFormatIssue( + magic === swap(MH_MAGIC) || magic === swap(MH_MAGIC_64) + ? "unsupported" + : "malformed", + magic === swap(MH_MAGIC) || magic === swap(MH_MAGIC_64) + ? "big-endian Mach-O slices are not supported" + : `slice at offset ${base} has no Mach-O header`, + ); +}; + +const decodeCommands = ( + bytes: Uint8Array, + header: { + readonly wide: boolean; + readonly cpuType: number; + readonly cpuSubtype: number; + readonly fileType: number; + readonly commandCount: number; + }, +): MachoSlice => { + // loader.h: load commands are 8-byte aligned in 64-bit images, 4 in 32-bit. + const alignment = header.wide ? 8 : 4; + const view = viewOf(bytes); + const slice: MachoSlice = { + architecture: architectureName(header.cpuType, header.cpuSubtype), + file_type: fileTypeName(header.fileType), + install_name: null, + dependencies: [], + rpaths: [], + dyld_environment: [], + code_signature_present: false, + }; + let offset = 0; + for (let index = 0; index < header.commandCount; index++) { + if (offset + 8 > bytes.byteLength) + throw new MachoFormatIssue( + "malformed", + `load command ${index} starts beyond sizeofcmds`, + ); + const command = view.getUint32(offset, true); + const size = view.getUint32(offset + 4, true); + if (size < 8 || size % alignment !== 0 || offset + size > bytes.byteLength) + throw new MachoFormatIssue( + "malformed", + `load command ${index} at offset ${offset} has invalid cmdsize ${size}`, + ); + const body = bytes.subarray(offset, offset + size); + decodeCommand(command, body, index, slice); + offset += size; + } + return slice; +}; + +const decodeCommand = ( + command: number, + body: Uint8Array, + index: number, + slice: MachoSlice, +): void => { + if (command === LC_ID_DYLIB) { + slice.install_name = commandString(body, 24, index); + return; + } + if (command === LC_RPATH) { + slice.rpaths.push(commandString(body, 12, index)); + return; + } + if (command === LC_DYLD_ENVIRONMENT) { + slice.dyld_environment.push(commandString(body, 12, index)); + return; + } + if (command === LC_CODE_SIGNATURE) { + // linkedit_data_command: cmd, cmdsize, dataoff, datasize. + if (body.byteLength < 16) + throw new MachoFormatIssue( + "malformed", + `load command ${index} is too short for LC_CODE_SIGNATURE`, + ); + slice.code_signature_present = true; + return; + } + const name = DEPENDENCY_COMMANDS.get(command); + if (name !== undefined) + slice.dependencies.push(decodeDependency(name, body, index)); +}; + +const decodeDependency = ( + command: MachoDependency["command"], + body: Uint8Array, + index: number, +): MachoDependency => { + if (body.byteLength < 24) + throw new MachoFormatIssue( + "malformed", + `load command ${index} is too short for a dylib command`, + ); + const view = viewOf(body); + const nameOffset = view.getUint32(8, true); + const usesFlags = + nameOffset === 28 && + body.byteLength >= 28 && + view.getUint32(12, true) === DYLIB_USE_MARKER; + const flags = usesFlags ? view.getUint32(24, true) : 0; + return { + command, + encoding: usesFlags ? "dylib_use_command" : "dylib_command", + install_name: commandString(body, usesFlags ? 28 : 24, index), + weak: + command === "LC_LOAD_WEAK_DYLIB" || (flags & DYLIB_USE_WEAK_LINK) !== 0, + upward: + command === "LC_LOAD_UPWARD_DYLIB" || (flags & DYLIB_USE_UPWARD) !== 0, + reexport: + command === "LC_REEXPORT_DYLIB" || (flags & DYLIB_USE_REEXPORT) !== 0, + delayed_init: (flags & DYLIB_USE_DELAYED_INIT) !== 0, + current_version: version(view.getUint32(16, true)), + compatibility_version: version(view.getUint32(20, true)), + }; +}; + +/** NUL-terminated UTF-8 string at a `lc_str` offset inside one command. */ +const commandString = ( + body: Uint8Array, + minimumOffset: number, + index: number, +): string => { + if (body.byteLength < 12) + throw new MachoFormatIssue( + "malformed", + `load command ${index} is too short to hold a string offset`, + ); + const offset = viewOf(body).getUint32(8, true); + if (offset < minimumOffset || offset >= body.byteLength) + throw new MachoFormatIssue( + "malformed", + `load command ${index} has string offset ${offset} outside the command`, + ); + const end = body.indexOf(0, offset); + if (end < 0) + throw new MachoFormatIssue( + "malformed", + `load command ${index} has an unterminated string`, + ); + try { + return new TextDecoder("utf-8", { fatal: true }).decode( + body.subarray(offset, end), + ); + } catch { + throw new MachoFormatIssue( + "malformed", + `load command ${index} has a string that is not UTF-8`, + ); + } +}; + +const readExact = async ( + readAt: ReadAt, + offset: number, + length: number, + label: string, +): Promise => { + const bytes = await readAt(offset, length); + if (bytes.byteLength < length) + throw new MachoFormatIssue("malformed", `${label} is truncated`); + return bytes; +}; + +const CPU_ARCH_ABI64 = 0x01000000; +const CPU_ARCH_ABI64_32 = 0x02000000; +const CPU_TYPE_X86 = 7; +const CPU_TYPE_ARM = 12; +const CPU_SUBTYPE_MASK = 0xff000000; +const CPU_SUBTYPE_ARM64E = 2; +const CPU_SUBTYPE_X86_64_H = 8; + +const architectureName = (cpuType: number, cpuSubtype: number): string => { + const subtype = cpuSubtype & ~CPU_SUBTYPE_MASK; + if (cpuType === (CPU_TYPE_ARM | CPU_ARCH_ABI64)) + return subtype === CPU_SUBTYPE_ARM64E ? "arm64e" : "arm64"; + if (cpuType === (CPU_TYPE_ARM | CPU_ARCH_ABI64_32)) return "arm64_32"; + if (cpuType === (CPU_TYPE_X86 | CPU_ARCH_ABI64)) + return subtype === CPU_SUBTYPE_X86_64_H ? "x86_64h" : "x86_64"; + if (cpuType === CPU_TYPE_X86) return "i386"; + if (cpuType === CPU_TYPE_ARM) return "arm"; + return `cpu-${cpuType.toString(16)}`; +}; + +const fileTypeName = (fileType: number): MachoSlice["file_type"] => + fileType === 2 + ? "execute" + : fileType === 6 + ? "dylib" + : fileType === 8 + ? "bundle" + : "other"; + +const version = (value: number): string => + `${value >>> 16}.${(value >>> 8) & 0xff}.${value & 0xff}`; + +const swap = (value: number): number => + (((value & 0xff) << 24) | + ((value & 0xff00) << 8) | + ((value >>> 8) & 0xff00) | + (value >>> 24)) >>> + 0; + +const safeNumber = (value: bigint): number => { + if (value > BigInt(Number.MAX_SAFE_INTEGER)) + throw new MachoFormatIssue("malformed", "FAT offset exceeds safe range"); + return Number(value); +}; + +const viewOf = (bytes: Uint8Array): DataView => + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); diff --git a/src/application/ArtifactExtraction.ts b/src/artifacts/extraction/ArtifactExtraction.ts similarity index 91% rename from src/application/ArtifactExtraction.ts rename to src/artifacts/extraction/ArtifactExtraction.ts index d33bbe90d4..58ad008f55 100644 --- a/src/application/ArtifactExtraction.ts +++ b/src/artifacts/extraction/ArtifactExtraction.ts @@ -1,20 +1,20 @@ import { lstat, realpath } from "node:fs/promises"; -import { canonicalDigest } from "../domain/comparisonSemantics.js"; -import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js"; +import { canonicalDigest } from "../../domain/comparisonSemantics.js"; +import { AsarArtifactReader } from "../AsarArtifactReader.js"; import { ArtifactPathRegistry, normalizeArtifactPath, -} from "../artifacts/ArtifactPaths.js"; +} from "../ArtifactPaths.js"; import { ArtifactReaderFailure, type ArtifactEntry, type ArtifactReader, -} from "../artifacts/ArtifactReader.js"; -import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js"; -import { SafeOutputTree } from "../artifacts/SafeOutputTree.js"; -import { ZipArtifactReader } from "../artifacts/ZipArtifactReader.js"; -import { MachOSliceArtifactReader } from "../artifacts/MachOSliceArtifactReader.js"; +} from "../ArtifactReader.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; +import { SafeOutputTree } from "../SafeOutputTree.js"; +import { ZipArtifactReader } from "../ZipArtifactReader.js"; +import { MachOSliceArtifactReader } from "../MachOSliceArtifactReader.js"; import { artifactExtractionResultSchema, type ArtifactExtractionResult, @@ -22,10 +22,10 @@ import { type ArtifactNode, type ArtifactOccurrence, type IntegrityContradiction, -} from "../domain/artifactGraph.js"; -import type { BinaryTarget } from "../domain/binaryTarget.js"; -import { scanArtifactInventory } from "./ArtifactInventory.js"; -import type { ArtifactIntegrityPolicy } from "./ArtifactInventory/types.js"; +} from "../../domain/artifactGraph.js"; +import type { BinaryTarget } from "../../domain/binaryTarget.js"; +import { scanArtifactInventory } from "../inventory/ArtifactInventory.js"; +import type { ArtifactIntegrityPolicy } from "../inventory/types.js"; /** Local extraction input with the output root chosen by the adapter. */ export interface ArtifactExtractionInput { @@ -221,7 +221,7 @@ const createExtractionResult = ( containment_verified: true, cleanup: { attempted: false, verified: true, residual_paths: [] }, provenance: [], - integrity_contradictions: writtenContradictions, + integrity_contradictions: inventory.integrityContradictions, limitations: [ "All regular files in the active artifact were materialized; nested archive contents remain represented by their containing file.", ...(writtenContradictions.length === 0 diff --git a/src/artifacts/inventory/ArtifactGraphConstruction.test.ts b/src/artifacts/inventory/ArtifactGraphConstruction.test.ts new file mode 100644 index 0000000000..93afadcb4e --- /dev/null +++ b/src/artifacts/inventory/ArtifactGraphConstruction.test.ts @@ -0,0 +1,14 @@ +import { expect, it } from "vitest"; + +import { compareDirectoryChildNames } from "./ArtifactGraphConstruction.js"; + +it("breaks locale-collation ties deterministically by code unit", () => { + const names = ["a\u200d.txt", "a.txt", "a\u200b.txt", "a\u200c.txt"]; + expect("a\u200d.txt".localeCompare("a.txt")).toBe(0); + expect(names.sort(compareDirectoryChildNames)).toEqual([ + "a.txt", + "a\u200b.txt", + "a\u200c.txt", + "a\u200d.txt", + ]); +}); diff --git a/src/application/ArtifactGraphConstruction.ts b/src/artifacts/inventory/ArtifactGraphConstruction.ts similarity index 97% rename from src/application/ArtifactGraphConstruction.ts rename to src/artifacts/inventory/ArtifactGraphConstruction.ts index 187d5d6776..a0d1d5e1c4 100644 --- a/src/application/ArtifactGraphConstruction.ts +++ b/src/artifacts/inventory/ArtifactGraphConstruction.ts @@ -1,13 +1,13 @@ -import { canonicalDigest } from "../domain/comparisonSemantics.js"; -import type { ArtifactEntry } from "../artifacts/ArtifactReader.js"; +import { canonicalDigest } from "../../domain/comparisonSemantics.js"; +import type { ArtifactEntry } from "../ArtifactReader.js"; import type { ArtifactCommand, ArtifactEdge, ArtifactNode, ArtifactOccurrence, -} from "../domain/artifactGraph.js"; -import { zipPackageFormatForPath } from "../domain/zipPackageFormat.js"; -import { mzWindowsHeaderOffset, parseDosMzHeader } from "../domain/dosMz.js"; +} from "../../domain/artifactGraph.js"; +import { zipPackageFormatForPath } from "../../domain/zipPackageFormat.js"; +import { mzWindowsHeaderOffset, parseDosMzHeader } from "../../domain/dosMz.js"; /** Mutable internal occurrence used until root-bound IDs are known. */ export interface MutableOccurrence { diff --git a/src/application/ArtifactInventory.ts b/src/artifacts/inventory/ArtifactInventory.ts similarity index 75% rename from src/application/ArtifactInventory.ts rename to src/artifacts/inventory/ArtifactInventory.ts index a7b5b181f9..3b121766b1 100644 --- a/src/application/ArtifactInventory.ts +++ b/src/artifacts/inventory/ArtifactInventory.ts @@ -3,22 +3,22 @@ import { realpath } from "node:fs/promises"; import { artifactInventoryResultSchema, type ArtifactInventoryResult, -} from "../domain/artifactGraph.js"; -import { abortIfNeeded } from "../artifacts/ArtifactHash.js"; -import { scanCanonicalArtifactInventory } from "./ArtifactInventory/scanCanonical.js"; +} from "../../domain/artifactGraph.js"; +import { abortIfNeeded } from "../ArtifactHash.js"; +import { scanCanonicalArtifactInventory } from "./scanCanonical.js"; import type { ArtifactIntegrityPolicy, ArtifactInventoryOptions, ArtifactInventorySnapshot, -} from "./ArtifactInventory/types.js"; +} from "./types.js"; -export { scanCanonicalArtifactInventory } from "./ArtifactInventory/scanCanonical.js"; +export { scanCanonicalArtifactInventory } from "./scanCanonical.js"; export type { ArtifactIntegrityPolicy, ArtifactInventoryOptions, ArtifactInventorySnapshot, -} from "./ArtifactInventory/types.js"; +} from "./types.js"; /** Inventory one local artifact and return every graph collection inline. */ export const inventoryArtifact = async ( diff --git a/src/application/ArtifactInventory/classify.ts b/src/artifacts/inventory/classify.ts similarity index 88% rename from src/application/ArtifactInventory/classify.ts rename to src/artifacts/inventory/classify.ts index 36e953712b..4217723461 100644 --- a/src/application/ArtifactInventory/classify.ts +++ b/src/artifacts/inventory/classify.ts @@ -1,7 +1,7 @@ import { open } from "node:fs/promises"; -import { classifyArtifactContent } from "../ArtifactGraphConstruction.js"; -import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../../artifacts/ArtifactHash.js"; +import { classifyArtifactContent } from "./ArtifactGraphConstruction.js"; +import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../ArtifactHash.js"; import type { ArtifactNode } from "../../domain/artifactGraph.js"; import { hasZipSignature, diff --git a/src/application/ArtifactInventory/policy.ts b/src/artifacts/inventory/policy.ts similarity index 100% rename from src/application/ArtifactInventory/policy.ts rename to src/artifacts/inventory/policy.ts diff --git a/src/application/ArtifactInventory/reader.ts b/src/artifacts/inventory/reader.ts similarity index 74% rename from src/application/ArtifactInventory/reader.ts rename to src/artifacts/inventory/reader.ts index f593acba09..18a0b27690 100644 --- a/src/application/ArtifactInventory/reader.ts +++ b/src/artifacts/inventory/reader.ts @@ -1,9 +1,9 @@ -import { AsarArtifactReader } from "../../artifacts/AsarArtifactReader.js"; -import type { ArtifactReader } from "../../artifacts/ArtifactReader.js"; -import { DirectoryArtifactReader } from "../../artifacts/DirectoryArtifactReader.js"; -import { MachOSliceArtifactReader } from "../../artifacts/MachOSliceArtifactReader.js"; -import { NativeDmgArtifactReader } from "../../artifacts/NativeDmgArtifactReader.js"; -import { ZipArtifactReader } from "../../artifacts/ZipArtifactReader.js"; +import { AsarArtifactReader } from "../AsarArtifactReader.js"; +import type { ArtifactReader } from "../ArtifactReader.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; +import { MachOSliceArtifactReader } from "../MachOSliceArtifactReader.js"; +import { NativeDmgArtifactReader } from "../NativeDmgArtifactReader.js"; +import { ZipArtifactReader } from "../ZipArtifactReader.js"; import type { ArtifactNode } from "../../domain/artifactGraph.js"; export const createReader = async ( diff --git a/src/application/ArtifactInventory/scanCanonical.ts b/src/artifacts/inventory/scanCanonical.ts similarity index 98% rename from src/application/ArtifactInventory/scanCanonical.ts rename to src/artifacts/inventory/scanCanonical.ts index b02005394b..2a746343df 100644 --- a/src/application/ArtifactInventory/scanCanonical.ts +++ b/src/artifacts/inventory/scanCanonical.ts @@ -5,7 +5,7 @@ import type { Stats } from "node:fs"; import { ArtifactReaderFailure, type ArtifactReader, -} from "../../artifacts/ArtifactReader.js"; +} from "../ArtifactReader.js"; import { artifactInventoryResultSchema, type ArtifactInventoryResult, @@ -20,10 +20,10 @@ import { rekeyOccurrences, rootOccurrenceFor, type MutableOccurrence, -} from "../ArtifactGraphConstruction.js"; +} from "./ArtifactGraphConstruction.js"; import { canonicalDigest } from "../../domain/comparisonSemantics.js"; import { classifyRoot } from "./classify.js"; -import { hashReadable, type HashResult } from "../../artifacts/ArtifactHash.js"; +import { hashReadable, type HashResult } from "../ArtifactHash.js"; import { createReader, inventoryLimitations } from "./reader.js"; import { scanReader, diff --git a/src/application/ArtifactInventory/scanReader.ts b/src/artifacts/inventory/scanReader.ts similarity index 96% rename from src/application/ArtifactInventory/scanReader.ts rename to src/artifacts/inventory/scanReader.ts index fe24fe408e..136f527b4f 100644 --- a/src/application/ArtifactInventory/scanReader.ts +++ b/src/artifacts/inventory/scanReader.ts @@ -3,13 +3,13 @@ import { isAbsolute } from "node:path"; import { ArtifactPathRegistry, normalizeArtifactPath, -} from "../../artifacts/ArtifactPaths.js"; +} from "../ArtifactPaths.js"; import { ArtifactReaderFailure, type ArtifactEntry, type ArtifactReader, -} from "../../artifacts/ArtifactReader.js"; -import { AsarArtifactReader } from "../../artifacts/AsarArtifactReader.js"; +} from "../ArtifactReader.js"; +import { AsarArtifactReader } from "../AsarArtifactReader.js"; import type { ArtifactNode } from "../../domain/artifactGraph.js"; import { classifyArtifactContent, @@ -17,12 +17,12 @@ import { createOccurrence, nearestParent, type MutableOccurrence, -} from "../ArtifactGraphConstruction.js"; +} from "./ArtifactGraphConstruction.js"; import { STRICT_INTEGRITY_POLICY, type ArtifactIntegrityPolicy, } from "./types.js"; -import { hashReadable } from "../../artifacts/ArtifactHash.js"; +import { hashReadable } from "../ArtifactHash.js"; export interface PendingIntegrityContradiction { readonly logicalPath: string; diff --git a/src/application/ArtifactInventory/types.ts b/src/artifacts/inventory/types.ts similarity index 100% rename from src/application/ArtifactInventory/types.ts rename to src/artifacts/inventory/types.ts diff --git a/src/artifacts/readStableArtifact.test.ts b/src/artifacts/readStableArtifact.test.ts new file mode 100644 index 0000000000..6c02e1b983 --- /dev/null +++ b/src/artifacts/readStableArtifact.test.ts @@ -0,0 +1,62 @@ +import { + lstat, + mkdtemp, + open, + rm, + symlink, + unlink, + writeFile, +} from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { expect, it } from "vitest"; +import { readStableArtifact } from "./readStableArtifact.js"; + +const checkChange = async ( + stage: "before-open" | "after-read" | "symlink-replacement", +): Promise => { + const root = await mkdtemp(join(tmpdir(), "rea-stable-read-")); + const path = join(root, "selected"); + await writeFile(path, "original evidence"); + let inspected = 0; + try { + const reading = readStableArtifact(path, 1024, undefined, { + async lstat(selected) { + inspected++; + if (stage === "after-read" && inspected === 2) await unlink(selected); + const stat = await lstat(selected, { bigint: true }); + if (inspected === 1 && stage !== "after-read") { + await unlink(selected); + if (stage === "symlink-replacement") + await symlink("missing", selected); + } + return stat; + }, + open, + }); + await expect(reading).rejects.toMatchObject({ reason: "integrity" }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}; + +it.each(["before-open", "after-read"] as const)( + "retains an actual post-validation %s change as integrity failure", + checkChange, +); + +it.skipIf(process.platform === "win32")( + "retains a post-validation symlink replacement as an integrity failure", + () => checkChange("symlink-replacement"), +); + +it("keeps initially missing inputs distinguishable from later changes", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-stable-missing-")); + try { + await expect( + readStableArtifact(join(root, "missing"), 1024), + ).rejects.toMatchObject({ code: "ENOENT" }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/src/artifacts/readStableArtifact.ts b/src/artifacts/readStableArtifact.ts index 83280550ea..560d00c959 100644 --- a/src/artifacts/readStableArtifact.ts +++ b/src/artifacts/readStableArtifact.ts @@ -1,16 +1,50 @@ import { createHash } from "node:crypto"; -import { constants } from "node:fs"; -import { lstat, open } from "node:fs/promises"; +import { constants, type BigIntStats } from "node:fs"; +import { lstat, open, type FileHandle } from "node:fs/promises"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; +/** Filesystem boundary for stable reads, including deterministic replacement probes. */ +export interface StableArtifactFileSystem { + lstat(path: string): Promise; + open(path: string, flags: number): Promise; +} + +const FILE_SYSTEM: StableArtifactFileSystem = { + lstat: (path) => lstat(path, { bigint: true }), + open, +}; + +const afterValidation = async ( + path: string, + stage: string, + read: () => Promise, +): Promise => { + try { + return await read(); + } catch (cause) { + if ( + cause instanceof Error && + "code" in cause && + ["ENOENT", "ENOTDIR", "ELOOP"].includes(String(cause.code)) + ) + throw new ArtifactReaderFailure( + "integrity", + `Artifact changed ${stage}: ${path}`, + { cause }, + ); + throw cause; + } +}; + /** Read one bounded regular file and reject replacement or in-place changes. */ export const readStableArtifact = async ( path: string, maximumBytes: number, signal?: AbortSignal, + fileSystem: StableArtifactFileSystem = FILE_SYSTEM, ): Promise<{ readonly bytes: Buffer; readonly sha256: string }> => { signal?.throwIfAborted(); - const before = await lstat(path, { bigint: true }); + const before = await fileSystem.lstat(path); if (!before.isFile() || before.isSymbolicLink()) throw new ArtifactReaderFailure( "path", @@ -21,9 +55,11 @@ export const readStableArtifact = async ( "limit", `Selected artifact exceeds ${String(maximumBytes)} bytes: ${path}`, ); - const file = await open( - path, - constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, + const file = await afterValidation(path, "before open", () => + fileSystem.open( + path, + constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, + ), ); try { const initial = await file.stat({ bigint: true }); @@ -59,7 +95,9 @@ export const readStableArtifact = async ( chunks.push(bytes); } const after = await file.stat({ bigint: true }); - const current = await lstat(path, { bigint: true }); + const current = await afterValidation(path, "during read", () => + fileSystem.lstat(path), + ); if ( [after, current].some( (stat) => diff --git a/src/browser/CdpCaptureDocuments.baseUrl.test.ts b/src/browser/CdpCaptureDocuments.baseUrl.test.ts index 7b3dc1868b..c2c9378b89 100644 --- a/src/browser/CdpCaptureDocuments.baseUrl.test.ts +++ b/src/browser/CdpCaptureDocuments.baseUrl.test.ts @@ -115,29 +115,6 @@ describe("DOM metadata document base URLs", () => { }); describe("empty form destinations with a document base URL", () => { - it.each([ - { nodeName: "FORM", attribute: "action" }, - { nodeName: "BUTTON", attribute: "formaction" }, - { nodeName: "INPUT", attribute: "formaction" }, - ])( - "resolves an empty $nodeName $attribute to the document", - ({ nodeName, attribute }) => { - const pageUrl = `${documentUrl}?selected=fixture#section`; - const baseUrl = `${origin}/assets/`; - const value = snapshot(baseUrl, "/assets/", pageUrl, ""); - value.strings[6] = nodeName; - value.strings[14] = attribute; - value.strings[15] = ""; - const result = capture(value); - expect(result.urls[3]).toMatchObject({ - attribute, - url: pageUrl, - destination_scope: "approved", - }); - expect(result.urls[1]?.url).toBe(baseUrl); - }, - ); - it("keeps an empty form action approved when the document base is foreign", () => { const foreign = "https://cdn.example.test/assets/"; const value = snapshot(foreign, foreign); diff --git a/src/browser/CdpCaptureEventHandlers.ts b/src/browser/CdpCaptureEventHandlers.ts index 0b93053664..23c06c2938 100644 --- a/src/browser/CdpCaptureEventHandlers.ts +++ b/src/browser/CdpCaptureEventHandlers.ts @@ -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, { diff --git a/src/browser/CdpCaptureEventState.ts b/src/browser/CdpCaptureEventState.ts index 654313961d..acff1d535c 100644 --- a/src/browser/CdpCaptureEventState.ts +++ b/src/browser/CdpCaptureEventState.ts @@ -11,6 +11,8 @@ export interface CdpCaptureEventsState { readonly scripts: Map; readonly executionContextFrames: Map; readonly network: Map; + readonly networkRequestTimestamps: Map; + readonly malformedRedirectRequestIds: Set; readonly allowedWebSockets: Set; console: WebPageInspection["console"]["events"]; websockets: WebPageInspection["network"]["websocket_events"]; diff --git a/src/browser/CdpCaptureEvents.redirectBodyShape.test.ts b/src/browser/CdpCaptureEvents.redirectBodyShape.test.ts new file mode 100644 index 0000000000..fb2da70a37 --- /dev/null +++ b/src/browser/CdpCaptureEvents.redirectBodyShape.test.ts @@ -0,0 +1,66 @@ +import { expect, it } from "vitest"; + +import { CdpCaptureEvents } from "./CdpCaptureEvents.js"; +import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; + +it("preserves an already captured response shape without marking it unavailable", () => { + const origin = "http://127.0.0.1:43127"; + const events = new CdpCaptureEvents( + inspectWebPageInputSchema.parse({ + cdp_endpoint: origin, + allowed_origins: [origin], + target_id: "allowed-page", + include_json_body_shapes: true, + }), + new Set([origin]), + ); + events.ingest({ + method: "Network.requestWillBeSent", + params: { + requestId: "request-1", + request: { + url: `${origin}/prior`, + method: "POST", + headers: { "Content-Type": "application/json" }, + postData: '{"request":true}', + }, + timestamp: 1, + }, + }); + events.ingest({ + method: "Network.responseReceived", + params: { + requestId: "request-1", + response: { + url: `${origin}/prior`, + status: 200, + mimeType: "application/json", + }, + }, + }); + events.ingestResponseBody("request-1", { + body: '{"response":true}', + base64Encoded: false, + }); + const capturedShape = events.network.get("request-1")?.body_shapes.response; + expect(capturedShape).not.toBeNull(); + + events.ingest({ + method: "Network.requestWillBeSent", + params: { + requestId: "request-1", + request: { url: `${origin}/continuation`, method: "GET" }, + redirectResponse: null, + timestamp: 2, + }, + }); + + expect(events.network.get("request-1")?.body_shapes.response).toEqual( + capturedShape, + ); + expect(events.network.get("request-1")?.body_shapes.status).toBe("included"); + expect(events.responseBodyRequestIds()).toEqual([]); + expect(events.completeness.snapshot().unavailable_sections).not.toContain( + "json_body_shapes", + ); +}); diff --git a/src/browser/CdpCaptureEvents.ts b/src/browser/CdpCaptureEvents.ts index e06a8722e3..bb24433eee 100644 --- a/src/browser/CdpCaptureEvents.ts +++ b/src/browser/CdpCaptureEvents.ts @@ -18,6 +18,8 @@ export class CdpCaptureEvents implements CdpCaptureEventsState { readonly scripts = new Map(); readonly executionContextFrames = new Map(); readonly network = new Map(); + readonly networkRequestTimestamps = new Map(); + readonly malformedRedirectRequestIds = new Set(); readonly allowedWebSockets = new Set(); 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); } } diff --git a/tests/boundary/browser/cdpSafeMetadata.test.ts b/src/browser/CdpSafeMetadata.test.ts similarity index 99% rename from tests/boundary/browser/cdpSafeMetadata.test.ts rename to src/browser/CdpSafeMetadata.test.ts index 56df3421ea..0cff4e0944 100644 --- a/tests/boundary/browser/cdpSafeMetadata.test.ts +++ b/src/browser/CdpSafeMetadata.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { safeResponseMetadata } from "../../../src/browser/CdpSafeMetadata.js"; +import { safeResponseMetadata } from "./CdpSafeMetadata.js"; const origin = "https://app.example.test"; diff --git a/src/browser/PlaywrightElectronActiveProvider.ts b/src/browser/PlaywrightElectronActiveProvider.ts index 9031369086..5aa0ef88cb 100644 --- a/src/browser/PlaywrightElectronActiveProvider.ts +++ b/src/browser/PlaywrightElectronActiveProvider.ts @@ -26,8 +26,13 @@ import { type OwnedProcessGroup, type ProcessCleanupResult, type ProcessLineageObservation, + type OwnershipSweepUnverifiedProcess, } from "../process/ProcessOwnership.js"; -import { observeOwnedProcessLineage } from "../process/ProcessOwnershipObservation.js"; +import { + observeOwnedProcessLineage, + prepareProcessOwnershipInspection, + systemProcessOwnershipHost, +} from "../process/ProcessOwnershipObservation.js"; import { selectCapturedProcessGroupIds } from "../process/ProcessOwnershipProcessTree.js"; import { runElectronActions, @@ -157,8 +162,13 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati try { if (options.signal?.aborted === true) throw new BrowserObservationError(OPERATION, "cancelled"); + await prepareProcessOwnershipInspection(options.signal); const startupDeadline = Date.now() + STARTUP_TIMEOUT_MS; const paths = await canonicalPaths(input); + const captureBaseline = + await systemProcessOwnershipHost.captureBaseline?.(options.signal); + if (options.signal?.aborted) + throw new BrowserObservationError(OPERATION, "cancelled"); application = await electron.launch({ executablePath: paths.executable, cwd: paths.root, @@ -183,6 +193,7 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati expectedParentPid: process.pid, expectedCommand: paths.executable, sweepTokenOwnedProcesses: true, + ...(captureBaseline === undefined ? {} : { captureBaseline }), }; const actions = await runElectronActions(application, input, options); const state = await runWithExecutionLimits( @@ -191,7 +202,13 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati ); outcome = ok(createResult(paths, input, actions, state)); } catch (cause: unknown) { - outcome = err(providerError(cause)); + outcome = err( + options.signal?.aborted === true && + (cause === options.signal.reason || + (cause instanceof Error && cause.name === "AbortError")) + ? new BrowserObservationError(OPERATION, "cancelled", { cause }) + : providerError(cause), + ); } if (application !== undefined) { if (ownership !== undefined) @@ -222,6 +239,17 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati ), ); if (closeError !== undefined) return err(providerError(closeError)); + if (outcome.ok && cleanup.unverified !== undefined) + outcome = ok({ + ...outcome.value, + limitations: [ + ...outcome.value.limitations, + ...cleanup.unverified.map( + ({ pid, diagnostic }) => + `Ownership of unrelated process ${String(pid)} could not be verified; it was left untouched: ${diagnostic}`, + ), + ], + }); } return outcome; } @@ -264,6 +292,7 @@ const cleanupElectronProcesses = async ( })), ); let signaled = false; + const unverified: OwnershipSweepUnverifiedProcess[] = []; for (const processGroupId of groupIds) { const cleanupOwnership: OwnedProcessGroup = processGroupId === ownership.processGroupId @@ -276,10 +305,38 @@ const cleanupElectronProcesses = async ( const result = await cleanupOwnedProcessGroup(cleanupOwnership); if (!result.cleaned) return result; signaled ||= result.signaled; + unverified.push(...(result.unverified ?? [])); } - const remaining = await verifyNoTokenOwnedProcesses(ownership.runId); + const remaining = await verifyNoTokenOwnedProcesses( + ownership.runId, + undefined, + ownership.captureBaseline, + { + leaderPid: ownership.leaderPid, + processGroupId: ownership.processGroupId, + ...(ownership.sampledProcessGroupIds === undefined + ? {} + : { sampledProcessGroupIds: ownership.sampledProcessGroupIds }), + }, + ); if (!remaining.cleaned) return remaining; - return { cleaned: true, signaled }; + unverified.push(...(remaining.unverified ?? [])); + return { + cleaned: true, + signaled, + ...(unverified.length === 0 + ? {} + : { + unverified: [ + ...new Map( + unverified.map((item) => [ + `${String(item.pid)}:${item.diagnostic}`, + item, + ]), + ).values(), + ], + }), + }; }; const createResult = ( diff --git a/src/browser/PlaywrightScenarioActions.test.ts b/src/browser/PlaywrightScenarioActions.test.ts deleted file mode 100644 index 2acd4322dc..0000000000 --- a/src/browser/PlaywrightScenarioActions.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { expect, it } from "vitest"; -import type { Page } from "playwright-core"; - -import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; -import { performPlaywrightScenarioAction } from "./PlaywrightScenarioActions.js"; -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" }, - start_url: { url: "https://app.example.test/" }, - actions: [ - { - step_id: "submit", - action: "click", - locator: { kind: "role", role: "button", name: "Submit" }, - }, - ], - }); - const calls: unknown[][] = []; - const page = { - getByRole: (...args: unknown[]) => { - calls.push(args); - return { click: async () => undefined }; - }, - } as unknown as Page; - const secrets = BrowserScenarioSecrets.resolve(scenario, {}); - if (secrets === undefined) throw new Error("No secret declarations expected"); - const action = scenario.actions.at(0); - if (action === undefined) throw new Error("Expected one scenario action"); - - await performPlaywrightScenarioAction({ - page, - action, - secrets, - }); - - expect(calls).toEqual([["button", { name: "Submit", exact: true }]]); -}); diff --git a/src/browser/assets/LocalWebScriptArtifacts.ts b/src/browser/assets/LocalWebScriptArtifacts.ts index 625246cebf..985756a738 100644 --- a/src/browser/assets/LocalWebScriptArtifacts.ts +++ b/src/browser/assets/LocalWebScriptArtifacts.ts @@ -1,5 +1,5 @@ import { lstat, realpath } from "node:fs/promises"; -import { dirname, isAbsolute, join, relative, sep } from "node:path"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { normalizeArtifactPath } from "../../artifacts/ArtifactPaths.js"; @@ -55,7 +55,7 @@ export class LocalWebScriptArtifacts implements WebScriptArtifactPort { "path", `Selected source path changes during normalization: ${selected.content.relative_path}`, ); - const root = dirname(input.manifest_path); + const root = resolve(dirname(input.manifest_path)); const sourcePath = join(root, "files", portable); targetPath = sourcePath; await assertContained(root, sourcePath); diff --git a/src/browser/history/CaptureFailures.test.ts b/src/browser/history/CaptureFailures.test.ts new file mode 100644 index 0000000000..71026ed6cf --- /dev/null +++ b/src/browser/history/CaptureFailures.test.ts @@ -0,0 +1,101 @@ +import { expect, it } from "vitest"; +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; +import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; +import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js"; +import { inspectWebNetworkCaptureInputSchema } from "../../domain/webNetworkCapture.js"; +import { historicalCaptureFailure } from "./CaptureFailures.js"; +import { redactExplicitFailure } from "../../domain/explicitSensitiveFailure.js"; +import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; + +it.each(["before open", "during read"])( + "preserves observed capture changes %s as retryable acquisition integrity failures", + (stage) => { + const path = "/selected/private-marker.har"; + const input = inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }); + const cause = new ArtifactReaderFailure( + "integrity", + `Artifact changed ${stage}: ${path}`, + ); + const error = historicalCaptureFailure(input, cause, "capture-read"); + const projected = projectAnalysisError(error); + expect(analysisErrorProjectionSchema.parse(projected)).toMatchObject({ + code: "artifact_changed", + category: "integrity_mismatch", + retryable: true, + details: { + operation: "inspect_web_network_capture", + path, + boundary: "stable-artifact-read", + reason: cause.message, + }, + }); + expect(projected.remediation.action).toContain("stable"); + const redacted = projectAnalysisError( + redactExplicitFailure(error, ["private-marker", "REDACTED"]), + ); + expect(redacted.code).toBe("artifact_changed"); + expect(JSON.stringify(redacted)).not.toContain("private-marker"); + expect(JSON.stringify(redacted)).not.toContain("REDACTED"); + }, +); + +it("retains actual command status and both diagnostic streams when cleanup also fails", () => { + const input = inspectWebNetworkCaptureInputSchema.parse({ + capture_path: "/selected/input.har", + format: "har", + }); + const cause = new OwnedCommandFailure( + "process", + "Observed command failure", + { + stdout: { text: "original provider output", bytes: 24 }, + stderr: { text: "private-marker provider reason", bytes: 30 }, + exitCode: 2, + signal: null, + }, + "Observed cleanup failure", + undefined, + ["pid:42"], + ); + const error = historicalCaptureFailure(input, cause, "decoder"); + expect(projectAnalysisError(error)).toMatchObject({ + code: "cleanup_incomplete", + details: { + diagnostics: { + previous_error: { + failure_kind: "process", + exit_code: 2, + stdout: "original provider output", + stderr: "private-marker provider reason", + }, + }, + }, + }); + const redacted = projectAnalysisError( + redactExplicitFailure(error, ["private-marker"]), + ); + expect(JSON.stringify(redacted)).not.toContain("private-marker"); + expect(redacted.code).toBe("cleanup_incomplete"); +}); +it.each(["io", "unavailable"] as const)( + "keeps %s read failures separate from malformed capture input", + (reason) => { + const input = inspectWebNetworkCaptureInputSchema.parse({ + capture_path: "/selected/input.har", + format: "har", + }); + const error = historicalCaptureFailure( + input, + new ArtifactReaderFailure(reason, "Observed acquisition failure"), + "capture-read", + ); + expect(projectAnalysisError(error)).toMatchObject({ + code: "execution_failure", + category: "execution_failure", + details: { diagnostics: { phase: "capture-read", failure_kind: reason } }, + }); + }, +); diff --git a/src/browser/history/CaptureFailures.ts b/src/browser/history/CaptureFailures.ts new file mode 100644 index 0000000000..fec40d5497 --- /dev/null +++ b/src/browser/history/CaptureFailures.ts @@ -0,0 +1,146 @@ +import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; +import { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisInputError, + AnalysisOutputError, + AnalysisTimeoutError, +} from "../../domain/analysisErrorCore.js"; +import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; +import { + WEB_NETWORK_CAPTURE_LIMITS, + type InspectWebNetworkCaptureInput, +} from "../../domain/webNetworkCapture.js"; +import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; +const OPERATION = "inspect_web_network_capture"; + +/** Preserve the origin and typed cause of a historical capture failure. */ +export const historicalCaptureFailure = ( + input: InspectWebNetworkCaptureInput, + cause: unknown, + phase: "capture-read" | "decoder", + options?: ExecutionOptions, +): AnalysisError => { + if (cause instanceof AnalysisError) return cause; + if (cause instanceof OwnedCommandFailure) { + if (cause.cleanupFailure !== null) + return new ProviderCleanupError( + input.format, + cause.resources, + { + reason: cause.cleanupFailure, + previous_error: { + failure_kind: cause.reason, + message: cause.message, + exit_code: cause.snapshot?.exitCode ?? null, + signal: cause.snapshot?.signal ?? null, + stdout: cause.snapshot?.stdout.text ?? null, + stderr: cause.snapshot?.stderr.text ?? null, + diagnostic_truncated: cause.snapshot?.diagnosticTruncated ?? null, + }, + }, + { operation: OPERATION, cause }, + ); + if (cause.reason === "cancelled") + return new AnalysisCancelledError(OPERATION); + if (cause.reason === "timeout") + return new AnalysisTimeoutError( + OPERATION, + WEB_NETWORK_CAPTURE_LIMITS.timeoutMs, + ); + if (cause.reason === "output-limit") + return new AnalysisOutputError(OPERATION, cause.message); + } + if (options?.signal?.aborted) return new AnalysisCancelledError(OPERATION); + if ( + phase === "capture-read" && + cause instanceof ArtifactReaderFailure && + cause.reason === "integrity" + ) + return new AnalysisArtifactChangedError( + OPERATION, + input.capture_path, + cause.message, + { cause }, + ); + if ( + phase === "capture-read" && + cause instanceof ArtifactReaderFailure && + cause.reason === "cancelled" + ) + return new AnalysisCancelledError(OPERATION); + if ( + phase === "capture-read" && + cause instanceof ArtifactReaderFailure && + (cause.reason === "limit" || + cause.reason === "format" || + cause.reason === "path") + ) + return new AnalysisInputError(OPERATION, { cause }, [ + { + path: ["capture_path"], + reason: cause.reason === "limit" ? "out_of_range" : "invalid_format", + message: + cause.reason === "limit" + ? `${cause.message}. Select a capture within the ${WEB_NETWORK_CAPTURE_LIMITS.inputBytes}-byte input budget.` + : cause.message, + ...(cause.reason === "limit" + ? { + expected: { + maximum_capture_bytes: WEB_NETWORK_CAPTURE_LIMITS.inputBytes, + }, + } + : {}), + }, + ]); + if ( + phase === "capture-read" && + cause instanceof Error && + "code" in cause && + (cause.code === "EACCES" || cause.code === "EPERM") + ) + return new AnalysisAccessDeniedError( + OPERATION, + input.capture_path, + cause.code, + { cause }, + ); + if ( + phase === "capture-read" && + cause instanceof Error && + "code" in cause && + ["ENOENT", "ENOTDIR"].includes(String(cause.code)) + ) + return new AnalysisInputError(OPERATION, { cause }, [ + { + path: ["capture_path"], + reason: "invalid_value", + message: `Selected capture could not be read (${String(cause.code)}): ${input.capture_path}.`, + }, + ]); + return new ProviderAdapterError(input.format, OPERATION, { + cause, + diagnostics: { + phase, + capture_path: input.capture_path, + reason: cause instanceof Error ? cause.message : String(cause), + ...(cause instanceof ArtifactReaderFailure + ? { failure_kind: cause.reason } + : {}), + ...(cause instanceof OwnedCommandFailure && cause.snapshot !== null + ? { + exit_code: cause.snapshot.exitCode ?? null, + signal: cause.snapshot.signal ?? null, + stdout: cause.snapshot.stdout.text, + stderr: cause.snapshot.stderr.text, + diagnostic_truncated: cause.snapshot.diagnosticTruncated ?? false, + } + : {}), + }, + }); +}; diff --git a/src/browser/history/CaptureFormatError.ts b/src/browser/history/CaptureFormatError.ts new file mode 100644 index 0000000000..fd7a9272d2 --- /dev/null +++ b/src/browser/history/CaptureFormatError.ts @@ -0,0 +1,10 @@ +/** Expected malformed, unsupported or oversized producer data at a historical decoder boundary. */ +export class CaptureFormatError extends Error { + constructor( + readonly reason: "format" | "unsupported" | "input-limit", + message: string, + readonly pointer = "", + ) { + super(message); + } +} diff --git a/src/browser/history/CaptureRedaction.test.ts b/src/browser/history/CaptureRedaction.test.ts new file mode 100644 index 0000000000..d6ce3bb1c6 --- /dev/null +++ b/src/browser/history/CaptureRedaction.test.ts @@ -0,0 +1,61 @@ +import { expect, it } from "vitest"; +import { CaptureRedaction } from "./CaptureRedaction.js"; + +it.each([ + { + value: " https://user:password@example.test/path ", + expected: " https://example.test/path ", + }, + { + value: "\tHTTPS://user:password@example.test/path\t", + expected: "\tHTTPS://example.test/path\t", + }, + { + value: " \t//user:password@example.test/path?token=ordinary#fragment\t ", + expected: " \t//example.test/path?token=ordinary#fragment\t ", + }, + { + value: "\r\n \v\fhttps://user:password@example.test/path", + expected: "\r\n \v\fhttps://example.test/path", + }, + { + value: "\u0000\u001f//user:password@example.test/path", + expected: "\u0000\u001f//example.test/path", + }, +])( + "excludes URL userinfo after optional whitespace while preserving the original header text: $value", + ({ value, expected }) => { + const redactor = new CaptureRedaction([]); + expect(redactor.text(value, "/response/headers/0/value", true)).toBe( + expected, + ); + expect(redactor.redactions).toEqual([ + { pointer: "/response/headers/0/value", reason: "transport-credential" }, + ]); + }, +); + +it("preserves the same URL-looking text in an ordinary extension", () => { + const redactor = new CaptureRedaction([]); + const value = " \thttps://user:password@example.test/path\t "; + expect(redactor.text(value, "/_extension")).toBe(value); + expect(redactor.redactions).toEqual([]); +}); + +it.each(["\ud800", "\udfff"])( + "does not invent replacement UTF-8 bytes for a non-scalar declaration", + (literal) => { + const redactor = new CaptureRedaction([literal]); + expect(redactor.sensitiveBytes(Buffer.from("\ufffd"))).toBe(false); + expect(redactor.text(literal, "/reported")).toBe(null); + }, +); + +it("matches scalar UTF-8 declarations and complete surrogate pairs exactly", () => { + expect( + new CaptureRedaction(["\ufffd"]).sensitiveBytes(Buffer.from("\ufffd")), + ).toBe(true); + expect( + new CaptureRedaction(["\ud83d\ude00"]).sensitiveBytes(Buffer.from("😀")), + ).toBe(true); +}); diff --git a/src/browser/history/CaptureRedaction.ts b/src/browser/history/CaptureRedaction.ts new file mode 100644 index 0000000000..86945aed0a --- /dev/null +++ b/src/browser/history/CaptureRedaction.ts @@ -0,0 +1,93 @@ +import type { WebNetworkCaptureRecord } from "../../domain/webNetworkCapture.js"; + +type Redaction = WebNetworkCaptureRecord["redactions"][number]; +const CREDENTIAL_HEADERS = new Set([ + "authorization", + "proxy-authorization", + "cookie", + "set-cookie", +]); + +/** Literal redaction and structural authentication exclusions preserve unrelated local evidence. */ +export class CaptureRedaction { + readonly redactions: Redaction[] = []; + readonly #sensitiveByteValues: readonly Buffer[]; + constructor(readonly sensitiveValues: readonly string[]) { + // An unpaired surrogate has no UTF-8 representation. Buffer.from would + // invent replacement-character bytes and incorrectly discard real evidence. + this.#sensitiveByteValues = sensitiveValues.flatMap((value) => { + const encoded = Buffer.from(value, "utf8"); + return encoded.toString("utf8") === value ? [encoded] : []; + }); + } + + /** Preserve exact text unless explicitly sensitive or known transport URL userinfo. */ + text(value: string, pointer: string, transportUrl = false): string | null { + let result = value; + if (transportUrl) { + const match = + /^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?\/\/)([^/?#]*@)/i.exec(result); + if (match !== null) { + result = `${match[1]}${result.slice(match[0].length)}`; + this.redactions.push({ pointer, reason: "transport-credential" }); + } + } + if ( + this.sensitiveValues.some( + (literal) => value.includes(literal) || result.includes(literal), + ) + ) { + this.redactions.push({ pointer, reason: "explicit-sensitive-value" }); + return null; + } + return result; + } + + /** Remove a credential field's value without retaining its digest or encoded form. */ + credential(pointer: string): null { + this.redactions.push({ pointer, reason: "transport-credential" }); + return null; + } + + /** Known HAR credential surfaces; unknown extensions are ordinary caller-selected evidence. */ + harCredential( + pointer: string, + parent: Readonly>, + ): boolean { + return ( + (/^\/(request|response)\/headers\/\d+\/value$/.test(pointer) && + typeof parent.name === "string" && + CREDENTIAL_HEADERS.has(parent.name.toLowerCase())) || + /^\/(request|response)\/cookies\/\d+\/value$/.test(pointer) + ); + } + + /** Only actual request/redirect fields and known transport header URLs declare URL semantics. */ + harTransportUrl( + pointer: string, + parent?: Readonly>, + ): boolean { + return ( + pointer === "/request/url" || + pointer === "/response/redirectURL" || + (/^\/(request|response)\/headers\/\d+\/value$/.test(pointer) && + typeof parent?.name === "string" && + ["location", "referer", "origin"].includes(parent.name.toLowerCase())) + ); + } + + /** Preserve bytes unless an explicitly declared UTF-8 literal occurs in them. */ + sensitiveBytes(bytes: Buffer): boolean { + return this.#sensitiveByteValues.some((value) => bytes.includes(value)); + } +} + +/** Escape a producer property for an RFC 6901 location. */ +export const capturePointer = (parent: string, key: string): string => + `${parent}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`; + +/** Structural record guard does not duck-type number/byte sidecar markers. */ +export const captureObject = ( + value: unknown, +): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/src/browser/history/CaptureRelease.ts b/src/browser/history/CaptureRelease.ts new file mode 100644 index 0000000000..4463befe9e --- /dev/null +++ b/src/browser/history/CaptureRelease.ts @@ -0,0 +1,13 @@ +/** Exact upstream package profile for unchanged offline HAR validation and numeric parsing. */ +export const HAR_CAPTURE_PROVIDER_IDENTITY = { + id: "har-schema/lossless-json", + name: "REA HAR capture adapter", + version: "har-schema@2.0.0;jsonc-parser@3.3.1;lossless-json@4.3.1", +} as const; + +/** Native executable profile verified with unchanged mitmproxy raw-state decoding. */ +export const MITMPROXY_CAPTURE_PROVIDER_IDENTITY = { + id: "mitmproxy-native-tnetstring", + name: "REA offline native mitmproxy adapter", + version: "12.2.3", +} as const; diff --git a/src/browser/history/HarCapture.test.ts b/src/browser/history/HarCapture.test.ts new file mode 100644 index 0000000000..4c29504a8a --- /dev/null +++ b/src/browser/history/HarCapture.test.ts @@ -0,0 +1,560 @@ +import { createHash } from "node:crypto"; +import { expect, it } from "vitest"; +import { historicalHar } from "../../../tests/fixtures/historicalHar.js"; +import { decodeHarCapture } from "./HarCapture.js"; + +it.each([ + { values: [] }, + { values: ["response"] }, + { values: ["content"] }, + { values: ["log"] }, + { values: ["entries"] }, +])( + "validates canonical base64 even when its parent property is sensitive: %j", + ({ values }) => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + const input = { + log: { + ...fixture.log, + entries: [ + { + ...first, + response: { + ...first.response, + content: { + size: 1, + mimeType: "application/octet-stream", + encoding: "base64", + text: "AR==", + }, + }, + }, + ], + }, + }; + expect(() => decodeHarCapture(JSON.stringify(input), values)).toThrow( + "not valid canonical base64", + ); + }, +); + +it.each(["log", "entries", "ntries"])( + "excludes extracted record payloads beneath the marked structural property %s", + (literal) => { + const decoded = decodeHarCapture(JSON.stringify(historicalHar()), [ + literal, + ]); + expect(decoded.total_records).toBeGreaterThan(0); + expect(decoded.container.records_pointer).toBe(null); + for (const [ordinal, record] of decoded.records.entries()) { + expect(record).toMatchObject({ + ordinal, + location: { kind: "unknown", reason: "explicit-sensitive-value" }, + reported: null, + binary_fields: [], + numeric_literals: [], + redactions: [{ pointer: "", reason: "explicit-sensitive-value" }], + }); + } + }, +); + +it("excludes sensitive property identities before emitting keys and sidecar coordinates", () => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + const result = decodeHarCapture( + JSON.stringify({ + ...fixture, + _extension: { + "api-secret/~": { numeric: 123, text: "api-secret" }, + kept: 7, + }, + log: { ...fixture.log, entries: [{ ...first, "api-secret": 42 }] }, + }), + ["api-secret"], + ); + expect(JSON.stringify(result)).not.toContain("api-secret"); + expect(result.container.reported).toMatchObject({ _extension: { kept: 7 } }); + expect(result.container.redactions).toContainEqual({ + pointer: "/_extension", + reason: "explicit-sensitive-value", + scope: "property-name", + }); + expect(result.records[0]?.redactions).toContainEqual({ + pointer: "", + reason: "explicit-sensitive-value", + scope: "property-name", + }); +}); + +it.each([ + { value: "REDACTED", sensitive: ["REDACTED"] }, + { value: "literal[", sensitive: ["["] }, + { value: "before secret after", sensitive: ["secret", "REDACTED"] }, + { value: "before secret after", sensitive: ["secret", "[RED"] }, +])( + "excludes sensitive text without introducing replacement literals: $value", + ({ value, sensitive }) => { + const fixture = historicalHar(); + const result = decodeHarCapture( + JSON.stringify({ ...fixture, _sensitive: value, _ordinary: "unmarked" }), + sensitive, + ); + expect(result.container.reported).toMatchObject({ + _sensitive: null, + _ordinary: "unmarked", + }); + expect(result.container.redactions).toContainEqual({ + pointer: "/_sensitive", + reason: "explicit-sensitive-value", + }); + }, +); + +it("excludes a declared literal introduced by stripping transport URL userinfo", () => { + const fixture = historicalHar(); + const entry = fixture.log.entries[0]; + if (entry === undefined) throw new Error("fixture missing"); + const url = "https://example.test/sensitive"; + const input = { + log: { + ...fixture.log, + entries: [ + { + ...entry, + request: { + ...entry.request, + url: "https://user:password@example.test/sensitive", + }, + }, + ], + }, + }; + const result = decodeHarCapture(JSON.stringify(input), [url]); + expect(result.records[0]?.reported).toMatchObject({ request: { url: null } }); +}); + +it.each([ + '"_duplicate":1,"_duplicate":1', + '"_duplicate":null,"_duplicate":null', + '"_duplicate":{"value":1},"_duplicate":{"value":1}', + '"_duplicate":1,"\\u005fduplicate":1', +])( + "rejects equal-valued duplicate JSON members before projection: %s", + (members) => { + const text = JSON.stringify(historicalHar()).replace( + '"cache":{}', + `"cache":{},${members}`, + ); + expect(() => decodeHarCapture(text, [])).toThrow("duplicate"); + }, +); + +it("rejects identical duplicate core fields before schema validation", () => { + const text = JSON.stringify(historicalHar()).replace( + '"version":"1.2"', + '"version":"1.2","version":"1.2"', + ); + expect(() => decodeHarCapture(text, [])).toThrow("duplicate"); +}); + +it("preserves prototype-named producer members through the capture boundary", () => { + const text = JSON.stringify(historicalHar()).replace( + '"cache":{}', + '"cache":{},"_extension":{"__proto__":{"preserved":7},"constructor":"ordinary"}', + ); + const result = decodeHarCapture(text, []); + const reported = result.records[0]?.reported; + expect(reported).toBeDefined(); + if ( + reported === undefined || + reported === null || + typeof reported !== "object" || + Array.isArray(reported) + ) + throw new Error("No HAR object record was retained"); + const extension = reported["_extension"]; + expect(extension).toEqual({ + ["__proto__"]: { preserved: 7 }, + constructor: "ordinary", + }); + expect(Object.getPrototypeOf(extension)).toBe(Object.prototype); + expect(Reflect.get(Object.prototype, "preserved")).toBeUndefined(); +}); + +it("preserves Unicode, offsets, sizes, duplicate URLs and opaque extensions without inventing bytes", () => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + fixture.log.entries.push(structuredClone(first)); + const result = decodeHarCapture( + JSON.stringify({ + ...fixture, + _extension: { isLosslessNumber: true, value: "ordinary" }, + }), + [], + ); + expect(result.total_records).toBe(2); + expect(result.records[0]?.reported).toEqual(fixture.log.entries[0]); + expect(result.records[0]?.binary_fields).toEqual([]); + expect(result.records[0]?.numeric_literals).toContainEqual({ + pointer: "/response/bodySize", + producer_type: "json-number", + literal: "99", + }); + expect(result.records[1]?.location).toEqual({ + kind: "json-pointer", + pointer: "/log/entries/1", + }); + expect(result.container.reported).toMatchObject({ + _extension: { isLosslessNumber: true, value: "ordinary" }, + log: { entries: null }, + }); +}); + +it.each(["9007199254740993", "4740993", "99"])( + "excludes explicitly sensitive numeric lexemes and their reported values: %s", + (literal) => { + const text = JSON.stringify(historicalHar()).replace( + '"cache":{}', + '"cache":{},"_big":9007199254740993', + ); + const result = decodeHarCapture(text, [literal]); + for (const record of result.records) { + expect( + record.numeric_literals.every( + (number) => !number.literal.includes(literal), + ), + ).toBe(true); + } + const pointer = literal === "99" ? "/response/bodySize" : "/_big"; + expect(result.records[0]?.redactions).toContainEqual({ + pointer, + reason: "explicit-sensitive-value", + }); + if (literal === "99") + expect(result.records[0]?.reported).toMatchObject({ + response: { bodySize: null }, + }); + else expect(result.records[0]?.reported).toMatchObject({ _big: null }); + }, +); + +it("keeps unsafe extension numeric lexemes and rejects unsafe mandatory schema numbers", () => { + const text = JSON.stringify(historicalHar()); + const result = decodeHarCapture( + text.replace('"cache":{}', '"cache":{},"_big":9007199254740993'), + [], + ); + expect(result.records[0]?.numeric_literals).toContainEqual({ + pointer: "/_big", + producer_type: "json-number", + literal: "9007199254740993", + }); + expect(result.records[0]?.reported).toMatchObject({ _big: null }); + expect(() => + decodeHarCapture( + text.replace('"bodySize":0', '"bodySize":9007199254740993'), + [], + ), + ).toThrow("HAR schema validation failed"); +}); + +it.each(["container", "record"] as const)( + "retains %s extension content without interpreting it as an HTTP body", + (location) => { + const fixture = historicalHar(); + const extension = { + response: { content: { encoding: "base64", text: "opaque extension" } }, + }; + const input = + location === "container" + ? { ...fixture, _extension: extension } + : { + log: { + ...fixture.log, + entries: fixture.log.entries.map((entry) => ({ + ...entry, + _extension: extension, + })), + }, + }; + const result = decodeHarCapture(JSON.stringify(input), []); + expect( + location === "container" + ? result.container.reported + : result.records[0]?.reported, + ).toMatchObject({ _extension: extension }); + expect(result.records[0]?.binary_fields).toEqual([]); + }, +); + +it("retains valid base64 bytes independently of declared sizes", () => { + const fixture = historicalHar(); + const entry = fixture.log.entries[0]; + if (entry === undefined) throw new Error("fixture missing"); + const text = JSON.stringify({ + log: { + ...fixture.log, + entries: [ + { + ...entry, + response: { + ...entry.response, + content: { + size: 999, + mimeType: "application/octet-stream", + encoding: "base64", + text: "AP9B\n", + }, + }, + }, + ], + }, + }); + const result = decodeHarCapture(text, []); + expect(result.records[0]?.binary_fields[0]).toMatchObject({ + pointer: "/response/content/text", + state: "retained", + content_base64: "AP9B", + bytes: 3, + }); + expect(result.records[0]?.reported).toMatchObject({ + response: { content: { size: 999, text: "AP9B\n" } }, + }); + expect(() => decodeHarCapture(text.replace("AP9B", "AR=="), [])).toThrow( + "not valid canonical base64", + ); +}); + +it("redacts actual authentication fields and explicit binary values while preserving similarly named extensions", () => { + const fixture = historicalHar(); + const entry = fixture.log.entries[0]; + if (entry === undefined) throw new Error("fixture missing"); + const raw = { + log: { + ...fixture.log, + entries: [ + { + ...entry, + request: { + ...entry.request, + url: "https://user:password@example.test/a?token=ordinary#fragment", + headers: [ + { name: "Authorization", value: "Bearer transport-secret" }, + { name: "X-Test", value: "a" }, + { name: "X-Test", value: "b" }, + ], + cookies: [{ name: "session", value: "cookie-secret" }], + }, + response: { + ...entry.response, + content: { + size: 5, + mimeType: "application/octet-stream", + encoding: "base64", + text: Buffer.from("chosen-private").toString("base64"), + }, + }, + _extension: { + response: { + headers: [{ name: "Authorization", value: "ordinary-extension" }], + }, + }, + }, + ], + }, + }; + const result = decodeHarCapture(JSON.stringify(raw), ["chosen-private"]); + const output = JSON.stringify(result); + for (const secret of [ + "password", + "transport-secret", + "cookie-secret", + Buffer.from("chosen-private").toString("base64"), + ]) + expect(output).not.toContain(secret); + expect(output).toContain("token=ordinary#fragment"); + expect(output).toContain("ordinary-extension"); + expect(result.records[0]?.reported).toMatchObject({ + request: { + headers: [ + { name: "Authorization", value: null }, + { name: "X-Test", value: "a" }, + { name: "X-Test", value: "b" }, + ], + }, + }); + expect(result.records[0]?.binary_fields[0]).toMatchObject({ + state: "redacted", + sha256: null, + content_base64: null, + }); +}); + +it.each([ + "{", + '{"log":{},"log":{}}', + JSON.stringify({ log: { version: "1.2", entries: [] } }), +])("rejects malformed producer data %s", (text) => { + expect(() => decodeHarCapture(text, [])).toThrow(); +}); + +it("preserves observed SaveHar null post-data without broadening arbitrary HAR producers", () => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + const log = { + ...fixture.log, + creator: { name: "mitmproxy", version: "12.2.3" }, + entries: [ + { + ...first, + request: { + ...first.request, + postData: { mimeType: "text/plain", text: null }, + }, + }, + ], + }; + expect( + decodeHarCapture(JSON.stringify({ log }), []).records[0]?.reported, + ).toMatchObject({ request: { postData: { text: null } } }); + expect(() => + decodeHarCapture( + JSON.stringify({ + log: { ...log, creator: { name: "other", version: "1" } }, + }), + [], + ), + ).toThrow("HAR schema validation failed"); +}); + +it.each([ + "c2VjcmV0", + "c2Vj", + createHash("sha256").update("secret").digest("hex"), +])( + "excludes sensitive derived byte representations from spaced HAR text: %s", + (literal) => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + const result = decodeHarCapture( + JSON.stringify({ + log: { + ...fixture.log, + entries: [ + { + ...first, + response: { + ...first.response, + content: { + size: 6, + mimeType: "application/octet-stream", + encoding: "base64", + text: "c2 VjcmV0", + }, + }, + }, + ], + }, + }), + [literal], + ); + expect(result.records[0]?.reported).toMatchObject({ + response: { content: { text: null } }, + }); + expect(result.records[0]?.binary_fields).toContainEqual({ + pointer: "/response/content/text", + representation: "har-base64-content", + state: "redacted", + content_base64: null, + bytes: null, + sha256: null, + }); + }, +); + +it("excludes a declared literal in encoded HAR text from its binary sidecar too", () => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + const log = { + ...fixture.log, + entries: [ + { + ...first, + response: { + ...first.response, + content: { + size: 3, + mimeType: "application/octet-stream", + encoding: "base64", + text: "AP9B", + }, + }, + }, + ], + }; + const result = decodeHarCapture(JSON.stringify({ log }), ["AP9B"]); + expect(JSON.stringify(result)).not.toContain("AP9B"); + expect(result.records[0]?.binary_fields[0]?.state).toBe("redacted"); +}); + +it("rejects excessive extension nesting instead of returning a partial capture", () => { + let extension: unknown = null; + for (let index = 0; index < 66; index++) extension = { child: extension }; + expect(() => + decodeHarCapture( + JSON.stringify({ ...historicalHar(), _extension: extension }), + [], + ), + ).toThrow("nesting budget"); +}); + +it.each([ + { name: "Location", side: "response" as const }, + { name: "Referer", side: "request" as const }, + { name: "Origin", side: "request" as const }, +])( + "excludes leading-OWS userinfo from a producer $name header", + ({ name, side }) => { + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + const value = + " \tHTTPS://user:password@example.test/path?token=ordinary#fragment\t "; + const result = decodeHarCapture( + JSON.stringify({ + log: { + ...fixture.log, + entries: [ + { + ...first, + [side]: { ...first[side], headers: [{ name, value }] }, + }, + ], + }, + }), + [], + ); + expect(result.records[0]?.reported).toMatchObject({ + [side]: { + headers: [ + { + name, + value: " \tHTTPS://example.test/path?token=ordinary#fragment\t ", + }, + ], + }, + }); + expect(result.records[0]?.redactions).toContainEqual({ + pointer: `/${side}/headers/0/value`, + reason: "transport-credential", + }); + }, +); diff --git a/src/browser/history/HarCapture.ts b/src/browser/history/HarCapture.ts new file mode 100644 index 0000000000..ddc31567c0 --- /dev/null +++ b/src/browser/history/HarCapture.ts @@ -0,0 +1,244 @@ +import { createHash } from "node:crypto"; +import { isSafeNumber, LosslessNumber } from "lossless-json"; +import { parseHarJson } from "./HarJson.js"; +import type { JsonValue } from "../../domain/jsonValue.js"; +import { + WEB_NETWORK_CAPTURE_LIMITS, + type WebNetworkCapture, + type WebNetworkCaptureRecord, +} from "../../domain/webNetworkCapture.js"; +import { CaptureFormatError } from "./CaptureFormatError.js"; +import { + CaptureRedaction, + captureObject, + capturePointer, +} from "./CaptureRedaction.js"; +import { createHarValidator } from "./HarSchema.js"; +import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; + +type DecodedCapture = Omit< + WebNetworkCapture, + "artifact" | "format" | "runtime_attribution" | "limitations" +>; +type Projection = Pick< + WebNetworkCaptureRecord, + "reported" | "numeric_literals" | "binary_fields" | "redactions" +>; + +/** Decode a retained HAR with exact numeric lexemes and unchanged upstream schema validation. */ +export const decodeHarCapture = ( + text: string, + sensitiveValues: readonly string[], +): DecodedCapture => { + const raw = parseHarJson(text); + const validation = projectHar(raw, [], false); + createHarValidator()(validation.reported); + if ( + !captureObject(raw) || + !captureObject(raw.log) || + !Array.isArray(raw.log.entries) + ) + throw new CaptureFormatError("format", "HAR must contain log.entries."); + if (raw.log.version !== "1.2") + throw new CaptureFormatError( + "unsupported", + "Only the pinned HAR 1.2 schema profile is supported.", + "/log/version", + ); + const entries = raw.log.entries; + const recordsExcluded = ["log", "entries"].some((key) => + sensitiveValues.some((literal) => key.includes(literal)), + ); + const container = projectHar( + { ...raw, log: { ...raw.log, entries: null } }, + sensitiveValues, + true, + ); + const records = entries.map((entry, ordinal): WebNetworkCaptureRecord => { + // Validate each original record even when a structural ancestor excludes it. + const projected = projectHar(entry, sensitiveValues, true); + const projection: Projection = recordsExcluded + ? { + reported: null, + numeric_literals: [], + binary_fields: [], + redactions: [{ pointer: "", reason: "explicit-sensitive-value" }], + } + : projected; + return { + ordinal, + location: recordsExcluded + ? { kind: "unknown", reason: "explicit-sensitive-value" } + : { kind: "json-pointer", pointer: `/log/entries/${ordinal}` }, + ...projection, + limitations: [ + "HAR text is producer-decoded Unicode. Original body bytes remain unknown unless content explicitly uses valid base64; declared sizes are preserved independently of retained bytes. mitmproxy 12.2.3 SaveHar may report null optional postData.text; that null is retained and omitted only from the upstream validation copy.", + ...(recordsExcluded + ? [ + "This record's payload and sidecars are excluded because its structural ancestor property was explicitly marked sensitive.", + ] + : []), + ], + }; + }); + return { + decoder: { ...HAR_CAPTURE_PROVIDER_IDENTITY }, + container: { + reported: container.reported, + numeric_literals: container.numeric_literals, + redactions: container.redactions, + records_pointer: recordsExcluded ? null : "/log/entries", + }, + total_records: records.length, + records, + }; +}; + +const projectHar = ( + value: unknown, + sensitiveValues: readonly string[], + redact: boolean, +): Projection => { + const redactor = new CaptureRedaction(sensitiveValues); + const numeric_literals: Projection["numeric_literals"] = []; + const binary_fields: Projection["binary_fields"] = []; + const visit = ( + item: unknown, + pointer: string, + depth: number, + parent?: Readonly>, + ): JsonValue => { + if (depth > WEB_NETWORK_CAPTURE_LIMITS.depth) + throw new CaptureFormatError( + "input-limit", + "HAR exceeds the 64-level complete-evidence nesting budget.", + pointer, + ); + if ( + redact && + parent !== undefined && + redactor.harCredential(pointer, parent) + ) + return redactor.credential(pointer); + if (item instanceof LosslessNumber) { + if (redact && redactor.text(item.value, pointer) === null) return null; + numeric_literals.push({ + pointer, + producer_type: "json-number", + literal: item.value, + }); + return isSafeNumber(item.value) ? Number(item.value) : null; + } + if (typeof item === "string") { + if ( + redact && + pointer === "/response/content/text" && + parent?.encoding === "base64" + ) { + const bytes = decodeHarBase64(item, pointer); + const contentBase64 = bytes.toString("base64"); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + const hidden = + redactor.sensitiveBytes(bytes) || + sensitiveValues.some( + (literal) => + item.includes(literal) || + contentBase64.includes(literal) || + sha256.includes(literal), + ); + binary_fields.push( + hidden + ? { + pointer, + representation: "har-base64-content", + state: "redacted", + content_base64: null, + bytes: null, + sha256: null, + } + : { + pointer, + representation: "har-base64-content", + state: "retained", + content_base64: contentBase64, + bytes: bytes.length, + sha256, + }, + ); + if (hidden) { + redactor.redactions.push({ + pointer, + reason: "explicit-sensitive-value", + }); + return null; + } + } + return redact + ? redactor.text( + item, + pointer, + redactor.harTransportUrl(pointer, parent), + ) + : item; + } + if (item === null || typeof item === "boolean") return item; + if (Array.isArray(item)) + return item.map((child, index) => + visit(child, capturePointer(pointer, String(index)), depth + 1), + ); + if (captureObject(item)) + return Object.fromEntries( + Object.entries(item).flatMap(([key, child]) => { + if ( + redact && + sensitiveValues.some((literal) => key.includes(literal)) + ) { + const beforeNumbers = numeric_literals.length; + const beforeBinaries = binary_fields.length; + const beforeRedactions = redactor.redactions.length; + visit(child, capturePointer(pointer, key), depth + 1, item); + numeric_literals.splice(beforeNumbers); + binary_fields.splice(beforeBinaries); + redactor.redactions.splice(beforeRedactions); + redactor.redactions.push({ + pointer, + reason: "explicit-sensitive-value", + scope: "property-name", + }); + return []; + } + return [ + [key, visit(child, capturePointer(pointer, key), depth + 1, item)], + ]; + }), + ); + throw new CaptureFormatError( + "format", + "HAR parser returned a non-JSON value.", + pointer, + ); + }; + return { + reported: visit(value, "", 0), + numeric_literals, + binary_fields, + redactions: redactor.redactions, + }; +}; + +const decodeHarBase64 = (value: string, pointer: string): Buffer => { + const compact = value.replace(/[\t\n\r ]/g, ""); + const bytes = Buffer.from(compact, "base64"); + if ( + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( + compact, + ) || + bytes.toString("base64") !== compact + ) + throw new CaptureFormatError( + "format", + "HAR content declares base64 but its text is not valid canonical base64 (ASCII whitespace is allowed).", + pointer, + ); + return bytes; +}; diff --git a/src/browser/history/HarCaptureAdapter.ts b/src/browser/history/HarCaptureAdapter.ts new file mode 100644 index 0000000000..b66167a5f4 --- /dev/null +++ b/src/browser/history/HarCaptureAdapter.ts @@ -0,0 +1,24 @@ +import { fileURLToPath } from "node:url"; +import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js"; +import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; + +/** HAR's parser/schema packages run in one bounded Node heap, without network or target execution. */ +export class HarCaptureAdapter implements HistoricalCaptureFormatAdapter { + readonly format = "har"; + readonly identity = HAR_CAPTURE_PROVIDER_IDENTITY; + async command( + requestPath: string, + ): Promise<{ command: string; arguments: readonly string[] }> { + return { + command: process.execPath, + arguments: [ + "--max-old-space-size=192", + "--max-semi-space-size=8", + "--v8-pool-size=1", + "--single-threaded", + fileURLToPath(new URL("./HarCaptureProcess.js", import.meta.url)), + requestPath, + ], + }; + } +} diff --git a/src/browser/history/HarCaptureProcess.ts b/src/browser/history/HarCaptureProcess.ts new file mode 100644 index 0000000000..0751d0b80f --- /dev/null +++ b/src/browser/history/HarCaptureProcess.ts @@ -0,0 +1,64 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { TextDecoder } from "node:util"; +import { z } from "zod"; +import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; +import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js"; +import { CaptureFormatError } from "./CaptureFormatError.js"; +import { decodeHarCapture } from "./HarCapture.js"; + +const requestSchema = z.strictObject({ + snapshot_path: z.string(), + reply_path: z.string(), + sensitive_values: z.array(z.string().min(1)), +}); + +/** Decode untrusted HAR in an owned, bounded heap rather than the long-lived MCP process. */ +const main = async (): Promise => { + const request = requestSchema.parse( + JSON.parse(await readFile(process.argv[2] ?? "", "utf8")), + ); + let reply: unknown; + try { + const snapshot = await readStableArtifact( + request.snapshot_path, + WEB_NETWORK_CAPTURE_LIMITS.inputBytes, + ); + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode( + snapshot.bytes, + ); + } catch { + throw new CaptureFormatError("format", "HAR is not valid UTF-8 JSON."); + } + reply = { + ok: true, + value: decodeHarCapture(text, request.sensitive_values), + }; + } catch (cause: unknown) { + reply = + cause instanceof CaptureFormatError + ? { + ok: false, + reason: cause.reason, + message: cause.message, + pointer: cause.pointer, + } + : { + ok: false, + reason: "decoder", + message: `HAR decoder failed unexpectedly (${cause instanceof Error ? cause.name : "unknown exception"}).`, + pointer: "", + }; + } + let text = JSON.stringify(reply); + if (Buffer.byteLength(text) > WEB_NETWORK_CAPTURE_LIMITS.outputBytes) + text = JSON.stringify({ + ok: false, + reason: "limit", + message: "HAR reply exceeds the complete-evidence output budget.", + pointer: "", + }); + await writeFile(request.reply_path, text, { flag: "wx", mode: 0o600 }); +}; +await main(); diff --git a/src/browser/history/HarJson.ts b/src/browser/history/HarJson.ts new file mode 100644 index 0000000000..2576309324 --- /dev/null +++ b/src/browser/history/HarJson.ts @@ -0,0 +1,116 @@ +import { visit } from "jsonc-parser"; +import { LosslessNumber } from "lossless-json"; +import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js"; +import { CaptureFormatError } from "./CaptureFormatError.js"; + +type Frame = + | { + kind: "object"; + pointer: string; + members: Map; + pendingKey?: string; + } + | { kind: "array"; pointer: string; items: unknown[] }; + +/** Materialize upstream JSON events without duplicate folding, prototype setters or numeric rounding. */ +export const parseHarJson = (text: string): unknown => { + const frames: Frame[] = []; + let result: unknown; + const valuePointer = (): string => { + const parent = frames.at(-1); + if (parent === undefined) return ""; + if (parent.kind === "array") + return `${parent.pointer}/${parent.items.length}`; + return parent.pendingKey === undefined + ? parent.pointer + : `${parent.pointer}/${parent.pendingKey.replaceAll("~", "~0").replaceAll("/", "~1")}`; + }; + const checkDepth = (): void => { + if (frames.length > WEB_NETWORK_CAPTURE_LIMITS.depth) + throw new CaptureFormatError( + "input-limit", + "HAR exceeds the 64-level complete-evidence nesting budget.", + valuePointer(), + ); + }; + const append = (value: unknown): void => { + const parent = frames.at(-1); + if (parent === undefined) result = value; + else if (parent.kind === "array") parent.items.push(value); + else { + if (parent.pendingKey === undefined) + throw new TypeError( + "JSON visitor omitted an object property identity.", + ); + parent.members.set(parent.pendingKey, value); + delete parent.pendingKey; + } + }; + visit( + text, + { + onObjectBegin: () => { + checkDepth(); + frames.push({ + kind: "object", + pointer: valuePointer(), + members: new Map(), + }); + }, + onObjectProperty: (key) => { + // Prototype-named members are kept as ordinary own properties: the + // members map keys them without prototype setters, and + // Object.fromEntries materializes them as own data properties. + const frame = frames.at(-1); + if (frame?.kind !== "object") + throw new TypeError( + "JSON visitor emitted a property outside an object.", + ); + if (frame.members.has(key)) + throw new CaptureFormatError( + "format", + "HAR JSON contains duplicate object members.", + ); + frame.pendingKey = key; + }, + onObjectEnd: () => { + const frame = frames.pop(); + if (frame?.kind !== "object") + throw new TypeError("JSON visitor ended an unexpected object."); + append(Object.fromEntries(frame.members)); + }, + onArrayBegin: () => { + checkDepth(); + frames.push({ kind: "array", pointer: valuePointer(), items: [] }); + }, + onArrayEnd: () => { + const frame = frames.pop(); + if (frame?.kind !== "array") + throw new TypeError("JSON visitor ended an unexpected array."); + append(frame.items); + }, + onLiteralValue: (value: unknown, offset, length) => { + checkDepth(); + if (typeof value === "number") + append(new LosslessNumber(text.slice(offset, offset + length))); + else if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) + append(value); + else + throw new TypeError("JSON visitor emitted an unsupported literal."); + }, + onError: () => { + throw new CaptureFormatError("format", "HAR is malformed JSON."); + }, + }, + { + disallowComments: true, + allowTrailingComma: false, + allowEmptyContent: false, + }, + ); + return result; +}; diff --git a/src/browser/history/HarSchema.ts b/src/browser/history/HarSchema.ts new file mode 100644 index 0000000000..069377f8ab --- /dev/null +++ b/src/browser/history/HarSchema.ts @@ -0,0 +1,87 @@ +import { createRequire } from "node:module"; +import { Ajv } from "ajv"; +import { z } from "zod"; +import { CaptureFormatError } from "./CaptureFormatError.js"; + +/** Validate using the unchanged, pinned upstream HAR 1.2 draft-06 schemas. */ +export const createHarValidator = (): ((value: unknown) => void) => { + const require = createRequire(import.meta.url); + const schemas = z + .record(z.string(), z.record(z.string(), z.unknown())) + .parse(require("har-schema")); + const ajv = new Ajv({ strict: false, allErrors: false, ownProperties: true }); + ajv.addMetaSchema(require("ajv/dist/refs/json-schema-draft-06.json")); + const plugin: unknown = require("ajv-formats"); + if (typeof plugin !== "function") + throw new Error("Pinned AJV formats plugin is unavailable"); + plugin(ajv); + for (const schema of Object.values(schemas)) ajv.addSchema(schema); + const validate = ajv.getSchema("har.json#"); + if (validate === undefined) + throw new Error("Pinned HAR root schema is unavailable"); + return (value) => { + allowMitmproxyAbsentPostData(value); + if (!validate(value)) { + const error = validate.errors?.[0]; + throw new CaptureFormatError( + "format", + `HAR schema validation failed: ${error?.keyword ?? "unknown constraint"}.`, + error?.instancePath ?? "", + ); + } + }; +}; + +/** Observed SaveHar 12.2.3 compatibility: validate omitted optional text while retaining reported null. */ +const allowMitmproxyAbsentPostData = (value: unknown): void => { + const parsed = z + .object({ + log: z.object({ + creator: z.object({ + name: z.literal("mitmproxy"), + version: z.literal("12.2.3"), + }), + entries: z.array(z.unknown()), + }), + }) + .safeParse(value); + if ( + !parsed.success || + typeof value !== "object" || + value === null || + !("log" in value) + ) + return; + const log = value.log; + if ( + typeof log !== "object" || + log === null || + !("entries" in log) || + !Array.isArray(log.entries) + ) + return; + for (const entry of log.entries) { + const candidate: unknown = entry; + if ( + typeof candidate !== "object" || + candidate === null || + !("request" in candidate) + ) + continue; + const request = candidate.request; + if ( + typeof request !== "object" || + request === null || + !("postData" in request) + ) + continue; + const body = request.postData; + if ( + typeof body === "object" && + body !== null && + "text" in body && + body.text === null + ) + delete body.text; + } +}; diff --git a/src/browser/history/HistoricalCaptureDecoder.ts b/src/browser/history/HistoricalCaptureDecoder.ts new file mode 100644 index 0000000000..e180ad6ad4 --- /dev/null +++ b/src/browser/history/HistoricalCaptureDecoder.ts @@ -0,0 +1,250 @@ +import { historicalCaptureFailure } from "./CaptureFailures.js"; +import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; +import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js"; +import { randomUUID } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { z } from "zod"; +import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; +import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { + WEB_NETWORK_CAPTURE_LIMITS, + webNetworkCaptureSchema, + type InspectWebNetworkCaptureInput, + type WebNetworkCapture, +} from "../../domain/webNetworkCapture.js"; +import { runOwnedCommand } from "../../process/OwnedCommand.js"; +import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; +import { redactExplicitFailure } from "../../domain/explicitSensitiveFailure.js"; + +const OPERATION = "inspect_web_network_capture"; +const decodedSchema = webNetworkCaptureSchema.omit({ + artifact: true, + format: true, + runtime_attribution: true, + limitations: true, +}); +const replySchema = z.discriminatedUnion("ok", [ + z.strictObject({ ok: z.literal(true), value: decodedSchema }), + z.strictObject({ + ok: z.literal(false), + reason: z.enum([ + "format", + "unsupported", + "input-limit", + "resource-limit", + "decoder", + "limit", + ]), + message: z.string(), + pointer: z.string(), + }), +]); + +/** One owned snapshot/process/root lifecycle for replaceable historical format decoders. */ +export class HistoricalCaptureDecoder { + constructor( + readonly adapters: readonly HistoricalCaptureFormatAdapter[], + readonly environment: Readonly, + readonly createRuntime: () => Promise< + Pick + > = () => PrivateRuntimeRoot.create({ prefix: "rea-web-capture-" }), + ) {} + + /** Snapshot the selected artifact, decode it offline, and release private credentials and processes. */ + async inspect( + input: InspectWebNetworkCaptureInput, + options?: ExecutionOptions, + ): Promise> { + let runtime: Pick | undefined; + let result: Result; + let phase: "capture-read" | "decoder" = "capture-read"; + try { + const adapter = this.adapters.find( + (candidate) => candidate.format === input.format, + ); + if (adapter === undefined) + throw new AnalysisCapabilityUnavailableError( + input.format, + OPERATION, + "No adapter is configured for the selected capture format.", + ); + const snapshot = await readStableArtifact( + input.capture_path, + WEB_NETWORK_CAPTURE_LIMITS.inputBytes, + options?.signal, + ); + phase = "decoder"; + runtime = await this.createRuntime(); + const snapshotPath = join(runtime.path, "capture.snapshot"); + const requestPath = join(runtime.path, "request.json"); + const replyPath = join(runtime.path, "reply.json"); + await writeFile(snapshotPath, snapshot.bytes, { + mode: 0o600, + flag: "wx", + ...(options?.signal === undefined ? {} : { signal: options.signal }), + }); + await writeFile( + requestPath, + JSON.stringify({ + snapshot_path: snapshotPath, + reply_path: replyPath, + sensitive_values: input.sensitive_values, + }), + { mode: 0o600, flag: "wx" }, + ); + const command = await adapter.command(requestPath, runtime.path); + const environment: NodeJS.ProcessEnv = { + ...this.environment, + UV_THREADPOOL_SIZE: "1", + }; + delete environment.NODE_OPTIONS; + await runOwnedCommand( + { + ...command, + runId: `rea-web-capture-${randomUUID()}`, + cwd: runtime.path, + hostEnvironment: environment, + }, + { + timeoutMs: WEB_NETWORK_CAPTURE_LIMITS.timeoutMs, + diagnosticBytes: 1024 * 1024, + }, + options?.signal === undefined ? {} : { signal: options.signal }, + ); + let reply: z.output; + try { + const replyFile = await readStableArtifact( + replyPath, + WEB_NETWORK_CAPTURE_LIMITS.outputBytes, + options?.signal, + ); + reply = replySchema.parse(JSON.parse(replyFile.bytes.toString("utf8"))); + } catch (cause: unknown) { + if (options?.signal?.aborted) + throw new AnalysisCancelledError(OPERATION); + throw new AnalysisOutputError( + OPERATION, + `Owned capture decoder reply failed for ${input.capture_path}: ${replyFailureReason(cause)}`, + { cause }, + ); + } + if (!reply.ok) { + if (reply.reason === "format") + throw new AnalysisInputError(OPERATION, undefined, [ + { + path: ["capture_path", reply.pointer], + reason: "invalid_format", + message: reply.message, + }, + ]); + if (reply.reason === "input-limit") + throw new AnalysisInputError(OPERATION, undefined, [ + { + path: ["capture_path", reply.pointer], + reason: "out_of_range", + message: reply.message, + expected: { + maximum_capture_nesting: WEB_NETWORK_CAPTURE_LIMITS.depth, + }, + }, + ]); + if (reply.reason === "decoder" || reply.reason === "resource-limit") + throw new ProviderAdapterError(input.format, OPERATION, { + diagnostics: { + phase: "decoder", + failure_kind: reply.reason, + reason: reply.message, + pointer: reply.pointer, + }, + }); + if (reply.reason === "unsupported") + throw new AnalysisCapabilityUnavailableError( + input.format, + OPERATION, + reply.message, + ); + throw new AnalysisOutputError(OPERATION, reply.message); + } + if ( + reply.value.decoder.id !== adapter.identity.id || + reply.value.decoder.name !== adapter.identity.name || + reply.value.decoder.version !== adapter.identity.version + ) + throw new AnalysisOutputError( + OPERATION, + "Capture decoder changed the selected upstream profile identity.", + ); + result = ok({ + ...reply.value, + artifact: { + path: input.capture_path, + sha256: snapshot.sha256, + bytes: snapshot.bytes.length, + }, + format: input.format, + runtime_attribution: "unknown", + limitations: [ + "This is retained producer evidence, without live browser transaction IDs, scenario provenance, execution attribution or deployment authenticity. Recorded URLs are never fetched.", + "Every record is decoded before selection; no partial success is returned on malformed input or a resource limit.", + "Known authentication headers/cookies and explicitly marked literal values are excluded. Unknown extension names do not establish sensitivity.", + "A 32 MiB input, 96 MiB reply and 64-level nesting budget bound complete evidence. HAR uses a 192 MiB Node old-generation heap; native decoding uses a 768 MiB Linux address-space limit. Each owned command has a 30-second deadline with independent cleanup.", + ], + }); + } catch (cause: unknown) { + result = err( + redactExplicitFailure( + historicalCaptureFailure(input, cause, phase, options), + input.sensitive_values, + ), + ); + } + if (runtime !== undefined) { + try { + await runtime.close(); + } catch (cause: unknown) { + return err( + redactExplicitFailure( + new ProviderCleanupError( + input.format, + [runtime.path], + { + capture_path: input.capture_path, + previous_error: result.ok + ? null + : projectAnalysisError(result.error), + reason: cause instanceof Error ? cause.message : String(cause), + }, + { operation: OPERATION }, + ), + input.sensitive_values, + ), + ); + } + } + return result.ok && options?.signal?.aborted + ? err(new AnalysisCancelledError(OPERATION)) + : result; + } +} + +const replyFailureReason = (cause: unknown): string => { + if (cause instanceof ArtifactReaderFailure) return cause.message; + if (cause instanceof z.ZodError) + return `Reply violates its schema at ${cause.issues[0]?.path.map(String).join(".") ?? "root"}.`; + if (cause instanceof SyntaxError) return "Reply is malformed JSON."; + if (cause instanceof Error && "code" in cause) + return `Reply file could not be read (${String(cause.code)}).`; + return "Reply could not be validated."; +}; diff --git a/src/browser/history/HistoricalCaptureFormatAdapter.ts b/src/browser/history/HistoricalCaptureFormatAdapter.ts new file mode 100644 index 0000000000..55281b5ec6 --- /dev/null +++ b/src/browser/history/HistoricalCaptureFormatAdapter.ts @@ -0,0 +1,15 @@ +import type { InspectWebNetworkCaptureInput } from "../../domain/webNetworkCapture.js"; + +/** Format-specific process profile keeps mature upstream code behind a replaceable adapter seam. */ +export interface HistoricalCaptureFormatAdapter { + readonly format: InspectWebNetworkCaptureInput["format"]; + readonly identity: { + readonly id: string; + readonly name: string; + readonly version: string; + }; + command( + requestPath: string, + runtimePath: string, + ): Promise<{ command: string; arguments: readonly string[] }>; +} diff --git a/src/browser/history/MitmproxyCaptureAdapter.ts b/src/browser/history/MitmproxyCaptureAdapter.ts new file mode 100644 index 0000000000..a3ebd66f73 --- /dev/null +++ b/src/browser/history/MitmproxyCaptureAdapter.ts @@ -0,0 +1,56 @@ +import { access } from "node:fs/promises"; +import { constants } from "node:fs"; +import { isAbsolute } from "node:path"; +import { fileURLToPath } from "node:url"; +import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; +import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js"; +import { MITMPROXY_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; +const OPERATION = "inspect_web_network_capture"; + +/** Native mitmproxy decoding is offline and uses only an explicitly configured Linux tool. */ +export class MitmproxyCaptureAdapter implements HistoricalCaptureFormatAdapter { + readonly format = "mitmproxy"; + readonly identity = MITMPROXY_CAPTURE_PROVIDER_IDENTITY; + constructor(readonly environment: Readonly) {} + async command( + requestPath: string, + runtimePath: string, + ): Promise<{ command: string; arguments: readonly string[] }> { + const command = this.environment.REA_MITMDUMP_COMMAND; + if ( + process.platform !== "linux" || + command === undefined || + !isAbsolute(command) + ) + throw new AnalysisCapabilityUnavailableError( + "mitmproxy", + OPERATION, + "Native capture decoding requires Linux and an absolute REA_MITMDUMP_COMMAND path to caller-supplied mitmdump 12.2.3; HAR needs no external engine.", + ); + try { + await access(command, constants.R_OK | constants.X_OK); + } catch (cause: unknown) { + throw new AnalysisCapabilityUnavailableError( + "mitmproxy", + OPERATION, + `Configured mitmdump executable is unavailable: ${command}.`, + { cause }, + ); + } + return { + command, + arguments: [ + "--no-server", + "-q", + "--set", + `confdir=${runtimePath}`, + "--set", + `rea_request_path=${requestPath}`, + "-s", + fileURLToPath( + new URL("../../../bridge/mitmproxy/capture.py", import.meta.url), + ), + ], + }; + } +} diff --git a/src/cli.ts b/src/cli.ts index 8ce590f055..6f6b84c3f2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,10 @@ +import { registerEvmCommands } from "./cli/evmCommands.js"; +import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js"; import { Cli } from "incur"; import { createLogger, parseLogLevel } from "./logger.js"; import { PRODUCT_IDENTITY } from "./identity.js"; +import { npxRegistrationCommand } from "./application/ClientRegistrationIdentity.js"; import { registerSetupCommands } from "./cli/setupCommands.js"; import { registerCoreAnalysisCommands } from "./cli/coreAnalysisCommands.js"; import { registerUtilityCommands } from "./cli/utilityCommands.js"; @@ -16,6 +19,7 @@ import { registerWebScriptCommands } from "./cli/webScriptCommands.js"; import { registerWebModuleCommands } from "./cli/webModuleCommands.js"; import { registerWebSourceLocationCommands } from "./cli/webSourceLocationCommands.js"; import { registerWebRuntimeCommands } from "./cli/webRuntimeCommands.js"; +import { registerWebNetworkCaptureCommands } from "./cli/webNetworkCaptureCommands.js"; import { registerJavaScriptRecoveryCommands } from "./cli/javascriptRecoveryCommands.js"; import { registerAdvancedBrowserCommands } from "./cli/browserAdvancedCommands.js"; import { registerBrowserScenarioCommands } from "./cliBrowserScenarioCommands.js"; @@ -23,6 +27,7 @@ import { registerElectronCommands } from "./cli/electronCommands.js"; import { registerJavaScriptRuntimeObservationCommands } from "./cliJavaScriptRuntimeCommands.js"; import { registerApplicationCommands } from "./cli/applicationCommands.js"; import type { CliInstance } from "./cli/types.js"; +import type { CliResultOutput } from "./cli/streamedJsonOutput.js"; /** * Build the one-shot Incur CLI without starting Hopper at import time. @@ -31,6 +36,7 @@ import type { CliInstance } from "./cli/types.js"; */ export const createCli = ( environment: Readonly> = process.env, + resultOutput?: CliResultOutput, ): CliInstance => { const logger = createLogger( "cli", @@ -42,7 +48,7 @@ export const createCli = ( version: PRODUCT_IDENTITY.packageVersion, description: "Reverse engineer anything from your terminal or agent.", mcp: { - command: PRODUCT_IDENTITY.mcpCommand, + command: npxRegistrationCommand().join(" "), instructions: "Ask what software, artifact, protocol, or behavior the user wants to understand, then choose the available investigation capabilities that can produce evidence.", }, @@ -50,12 +56,14 @@ export const createCli = ( }); registerSetupCommands(cli, logger); - registerCoreAnalysisCommands(cli, logger); + registerCoreAnalysisCommands(cli, logger, resultOutput); registerUtilityCommands(cli, logger, environment); + registerEvmCommands(cli, logger, environment); registerArtifactCommands(cli, logger); registerManagedCommands(cli, logger); registerAndroidCommands(cli, logger, environment); registerFirmwareCommands(cli, logger, environment); + registerBinaryDiagnosticsCommands(cli, logger, environment); registerEvidenceCommands(cli, logger); registerProcessCommands(cli, logger, environment); registerBrowserCommands(cli, logger); @@ -63,10 +71,11 @@ export const createCli = ( registerWebModuleCommands(cli, logger, environment); registerWebSourceLocationCommands(cli, logger, environment); registerWebRuntimeCommands(cli, logger); + registerWebNetworkCaptureCommands(cli, logger, environment); registerJavaScriptRecoveryCommands(cli, logger, environment); registerAdvancedBrowserCommands(cli, logger); registerBrowserScenarioCommands(cli, logger); - registerElectronCommands(cli, logger); + registerElectronCommands(cli, logger, resultOutput); registerJavaScriptRuntimeObservationCommands(cli, logger); registerApplicationCommands(cli, logger); return cli; diff --git a/src/cli/artifactCommands.ts b/src/cli/artifactCommands.ts index c022d45add..38489a50ad 100644 --- a/src/cli/artifactCommands.ts +++ b/src/cli/artifactCommands.ts @@ -1,7 +1,8 @@ import { z } from "incur"; +import { artifactIntegrityPolicySchema } from "../domain/artifactGraph.js"; import { runProviderAnalysis } from "../composition/directAnalysis.js"; -import { createArtifactExtractionDestination } from "../application/ArtifactExtractionDestination.js"; +import { createArtifactExtractionDestination } from "../application/artifacts/ArtifactExtractionDestination.js"; import { logCliCommand } from "../cliLogging.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import type { Logger } from "../logger.js"; @@ -15,6 +16,7 @@ export const registerArtifactCommands = ( registerExtractionCommand(cli, logger); registerInterfaceBuilderCommand(cli, logger); registerAssetCatalogCommand(cli, logger); + registerDylibResolutionCommand(cli, logger); cli.command(CLI_COMMANDS.inspectKeyedArchive, { description: "Inspect a Foundation keyed archive as an object-reference graph", @@ -58,6 +60,45 @@ export const registerArtifactCommands = ( }); }; +const registerDylibResolutionCommand = ( + cli: CliInstance, + logger: Logger, +): void => { + cli.command(CLI_COMMANDS.traceDylibResolution, { + description: + "Trace dyld load-path resolution for a Mach-O or every executable in an app bundle", + args: z.object({ + path: z.string().describe("Mach-O file or Apple .app bundle path"), + }), + options: z.object({ + root: z + .array(z.string()) + .optional() + .describe( + "Process root relative to the bundle or the Mach-O's directory (repeatable)", + ), + architecture: z + .enum(["arm64", "arm64e", "x86_64"]) + .optional() + .describe("Trace only this slice of each root"), + }), + run: ({ args, options }) => + logCliCommand(logger, CLI_COMMANDS.traceDylibResolution, () => + runProviderAnalysis( + args.path, + "trace_dylib_resolution", + { + ...(options.root === undefined ? {} : { roots: options.root }), + ...(options.architecture === undefined + ? {} + : { architecture: options.architecture }), + }, + logger, + ), + ), + }); +}; + const registerAssetCatalogCommand = ( cli: CliInstance, logger: Logger, @@ -154,14 +195,10 @@ const registerExtractionCommand = (cli: CliInstance, logger: Logger): void => { path: z.string().describe("Application or package path"), }), options: z.object({ - integrityPolicy: z - .enum(["fail", "record-and-continue"]) - .default("fail") - .describe("Behavior when declared artifact integrity does not match"), + integrityPolicy: artifactIntegrityPolicySchema.describe( + "Behavior when declared artifact integrity does not match", + ), }), - alias: { - integrityPolicy: "integrity-policy", - }, run: ({ args, options }) => logCliCommand(logger, "extract-artifact", () => runProviderAnalysis( @@ -185,14 +222,10 @@ const registerInspectionCommand = (cli: CliInstance, logger: Logger): void => { path: z.string().describe("Application or package path"), }), options: z.object({ - integrityPolicy: z - .enum(["fail", "record-and-continue"]) - .default("fail") - .describe("Behavior when declared artifact integrity does not match"), + integrityPolicy: artifactIntegrityPolicySchema.describe( + "Behavior when declared artifact integrity does not match", + ), }), - alias: { - integrityPolicy: "integrity-policy", - }, run: ({ args, options }) => logCliCommand(logger, "inspect-artifact", () => runProviderAnalysis( diff --git a/src/cli/binaryDiagnosticsCommands.ts b/src/cli/binaryDiagnosticsCommands.ts new file mode 100644 index 0000000000..560a119356 --- /dev/null +++ b/src/cli/binaryDiagnosticsCommands.ts @@ -0,0 +1,63 @@ +import { z } from "incur"; +import { resolve } from "node:path"; +import { + createRecordedCrashService, + createBinaryLayoutService, +} from "../composition/binaryDiagnostics.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { logCliCommand } from "../cliLogging.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import type { Logger } from "../logger.js"; +import type { CliInstance } from "./types.js"; +import { withCommandCancellation } from "./commandCancellation.js"; + +/** Inspect operator-selected files through the same shared workflow as MCP. */ +export const registerBinaryDiagnosticsCommands = ( + cli: CliInstance, + logger: Logger, + environment: Readonly, +): void => { + const service = createBinaryLayoutService(environment); + cli.command(CLI_COMMANDS.inspectBinaryLayout, { + description: + "Inspect binary file/linked layout without executing the selected object", + args: z.object({ + path: z.string().describe("Local ELF binary or relocatable object"), + }), + run: ({ args }) => + withCommandCancellation((signal) => + logCliCommand(logger, CLI_COMMANDS.inspectBinaryLayout, async () => { + const result = await service.inspect( + { path: resolve(args.path) }, + { signal }, + ); + return result.ok ? result.value : projectAnalysisError(result.error); + }), + ), + }); + const recorded = createRecordedCrashService(environment); + cli.command(CLI_COMMANDS.inspectRecordedCrash, { + description: + "Inspect supplied core registers/signals and optional debugger mapping evidence", + args: z.object({ path: z.string().describe("Local Linux amd64 ELF core") }), + options: z.object({ + debuggerContext: z + .boolean() + .default(false) + .describe("Add BYO core-only GDB/pwndbg mapping candidates"), + }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand(logger, CLI_COMMANDS.inspectRecordedCrash, async () => { + const result = await recorded.inspect( + { + path: resolve(args.path), + include_debugger_context: options.debuggerContext, + }, + { signal }, + ); + return result.ok ? result.value : projectAnalysisError(result.error); + }), + ), + }); +}; diff --git a/src/cli/browserAdvancedCommands.ts b/src/cli/browserAdvancedCommands.ts index 2d299503f2..0fd49960e1 100644 --- a/src/cli/browserAdvancedCommands.ts +++ b/src/cli/browserAdvancedCommands.ts @@ -10,7 +10,10 @@ import { } from "../application/BrowserObservationService.js"; import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js"; import { logCliCommand } from "../cliLogging.js"; -import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { + AnalysisInputError, + type AnalysisInputIssue, +} from "../domain/analysisErrorCore.js"; import { browserCaptureComparisonInputSchema } from "../domain/browserCaptureComparison.js"; import { discoverWebMcpToolsInputSchema } from "../domain/webMcpDiscovery.js"; import { @@ -89,19 +92,32 @@ const registerCaptureDiff = ( }), run: ({ args, options }) => logCliCommand(logger, "compare-web-captures", async () => { - const before = parseJson(args.beforeJson); - const after = parseJson(args.afterJson); + const before = safeParseJson(args.beforeJson); + const after = safeParseJson(args.afterJson); + const normalization = safeParseJson(options.normalizationJson); + if (!before.ok) + return inputError("compare_web_captures", [ + invalidJsonIssue("before"), + ]); + if (!after.ok) + return inputError("compare_web_captures", [ + invalidJsonIssue("after"), + ]); + if (!normalization.ok) + return inputError("compare_web_captures", [ + invalidJsonIssue("normalization"), + ]); const scenarioComparison = browserCaptureComparisonInputSchema.safeParse({ - before_scenario: before, - after_scenario: after, - normalization: parseJson(options.normalizationJson), + before_scenario: before.value, + after_scenario: after.value, + normalization: normalization.value, }); const parsed = scenarioComparison.success ? scenarioComparison : browserCaptureComparisonInputSchema.safeParse({ - before, - after, + before: before.value, + after: after.value, }); if (!parsed.success) return inputError("compare_web_captures"); const result = await compareWebCaptureEvidence( @@ -186,5 +202,14 @@ const parseJson = (value: string): unknown => { return parsed.ok ? parsed.value : undefined; }; -const inputError = (operation: string): JsonValue => - browserCliError(new AnalysisInputError(operation)); +const invalidJsonIssue = (field: string): AnalysisInputIssue => ({ + path: [field], + reason: "invalid_format", + expected: "JSON", +}); + +const inputError = ( + operation: string, + issues: readonly AnalysisInputIssue[] = [], +): JsonValue => + browserCliError(new AnalysisInputError(operation, undefined, issues)); diff --git a/src/cli/coreAnalysisCommands.ts b/src/cli/coreAnalysisCommands.ts index da50ca8302..e323348da0 100644 --- a/src/cli/coreAnalysisCommands.ts +++ b/src/cli/coreAnalysisCommands.ts @@ -2,12 +2,14 @@ import type { Logger } from "../logger.js"; import { registerCoreBinaryCommands } from "./coreBinaryCommands.js"; import { registerCoreNativeCommands } from "./coreNativeCommands.js"; import type { CliInstance } from "./types.js"; +import type { CliResultOutput } from "./streamedJsonOutput.js"; /** Register core binary and native deep-analysis CLI commands. */ export const registerCoreAnalysisCommands = ( cli: CliInstance, logger: Logger, + resultOutput?: CliResultOutput, ): void => { - registerCoreBinaryCommands(cli, logger); + registerCoreBinaryCommands(cli, logger, resultOutput); registerCoreNativeCommands(cli, logger); }; diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts index 4ff3d86493..50f78b3bd3 100644 --- a/src/cli/coreBinaryCommands.ts +++ b/src/cli/coreBinaryCommands.ts @@ -1,4 +1,5 @@ import { z } from "incur"; +import { artifactIntegrityPolicySchema } from "../domain/artifactGraph.js"; import { stat } from "node:fs/promises"; import { resolve } from "node:path"; @@ -13,13 +14,18 @@ import { } from "./options.js"; import type { CliInstance } from "./types.js"; import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js"; +import type { + CliCommandOutput, + CliResultOutput, +} from "./streamedJsonOutput.js"; /** Register provider-neutral binary overview and procedure CLI commands. */ export const registerCoreBinaryCommands = ( cli: CliInstance, logger: Logger, + resultOutput?: CliResultOutput, ): void => { - registerOverviewCommands(cli, logger); + registerOverviewCommands(cli, logger, resultOutput); registerFunctionCommand(cli, logger); registerInstructionsCommand(cli, logger); registerSearchCommand(cli, logger); @@ -27,7 +33,11 @@ export const registerCoreBinaryCommands = ( registerTraceCommand(cli, logger); }; -const registerOverviewCommands = (cli: CliInstance, logger: Logger): void => { +const registerOverviewCommands = ( + cli: CliInstance, + logger: Logger, + resultOutput?: CliResultOutput, +): void => { const overviewOptions = z.object({ snapshot: z .string() @@ -43,19 +53,20 @@ const registerOverviewCommands = (cli: CliInstance, logger: Logger): void => { path: z.string().describe("App, program, or analysis database path"), }), options: overviewOptions.extend({ - integrityPolicy: z - .enum(["fail", "record-and-continue"]) - .default("fail") - .describe( - "Behavior when a routed JavaScript application declares integrity that does not match", - ), + integrityPolicy: artifactIntegrityPolicySchema.describe( + "Behavior when declared JavaScript artifact integrity does not match", + ), }), - alias: { - integrityPolicy: "integrity-policy", - }, - run: ({ args, options }) => + run: ({ args, options, format }) => logCliCommand(logger, "analyze", () => - runRoutedOverview(args.path, options, logger), + runRoutedOverview( + args.path, + options, + logger, + resultOutput === undefined + ? undefined + : { output: resultOutput, command: CLI_COMMANDS.analyze, format }, + ), ), }); cli.command(CLI_COMMANDS.inspect, { @@ -120,6 +131,7 @@ const runRoutedOverview = async ( readonly integrityPolicy?: "fail" | "record-and-continue" | undefined; }, logger: Logger, + output?: CliCommandOutput, ) => { if ( options.provider === undefined && @@ -127,10 +139,13 @@ const runRoutedOverview = async ( options["target-format"] === undefined && (await isJavaScriptApplicationPath(path)) ) - return runCliJavaScriptApplicationAnalysis({ - input_path: resolve(path), - integrity_policy: options.integrityPolicy ?? "fail", - }); + return runCliJavaScriptApplicationAnalysis( + { + input_path: resolve(path), + integrity_policy: options.integrityPolicy ?? "fail", + }, + output, + ); return runDirectAnalysis( path, "binary_overview", diff --git a/src/cli/electronCommands.ts b/src/cli/electronCommands.ts index 4d076a03d8..6cc4e4d725 100644 --- a/src/cli/electronCommands.ts +++ b/src/cli/electronCommands.ts @@ -14,24 +14,27 @@ import { import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +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, } from "../cliObservationOptions.js"; import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js"; +import type { CliResultOutput } from "./streamedJsonOutput.js"; /** Register CLI equivalents of the Electron MCP tools. */ export const registerElectronCommands = ( cli: ReturnType, logger: Logger, + resultOutput?: CliResultOutput, ): void => { registerElectronObservationCommands(cli, logger); registerElectronActiveCommand(cli, logger); - registerJavaScriptApplicationCommand(cli, logger); + registerJavaScriptApplicationCommand(cli, logger, resultOutput); registerJavaScriptRuntimeReconciliationCommand(cli, logger); }; @@ -56,9 +59,18 @@ 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("capture_electron_scenario"); + if (!parsed.success) + return inputError( + "capture_electron_scenario", + parsed.error.issues, + input.value, + ); const { createElectronScenarioProvider } = await import("../composition/electronScenario.js"); const result = await captureElectronScenario( @@ -162,6 +174,7 @@ const registerElectronPageInspection = ( const registerJavaScriptApplicationCommand = ( cli: ReturnType, logger: Logger, + resultOutput?: CliResultOutput, ): void => { cli.command(CLI_COMMANDS.analyzeJavaScriptApplication, { description: @@ -170,13 +183,22 @@ const registerJavaScriptApplicationCommand = ( path: z.string().describe("ASAR or extracted application path"), }), options: javascriptApplicationOptions, - run: ({ args, options }) => + run: ({ args, options, format }) => logCliCommand(logger, CLI_COMMANDS.analyzeJavaScriptApplication, () => - runCliJavaScriptApplicationAnalysis({ - input_path: args.path, - format: options.artifactFormat, - integrity_policy: options.integrityPolicy, - }), + runCliJavaScriptApplicationAnalysis( + { + input_path: args.path, + format: options.artifactFormat, + integrity_policy: options.integrityPolicy, + }, + resultOutput === undefined + ? undefined + : { + output: resultOutput, + command: CLI_COMMANDS.analyzeJavaScriptApplication, + format, + }, + ), ), }); }; @@ -187,8 +209,18 @@ const electronObservationContext = async () => { return { provider: createElectronObservationProvider() }; }; -const inputError = (operation: string): JsonValue => - cliError(new AnalysisInputError(operation)); +const inputError = ( + operation: string, + issues?: Parameters[0], + input?: unknown, +): JsonValue => + cliError( + new AnalysisInputError( + operation, + undefined, + issues === undefined ? [] : projectInputIssues(issues, input), + ), + ); const cliError = ( error: Parameters[0], diff --git a/src/cli/evmCommands.ts b/src/cli/evmCommands.ts new file mode 100644 index 0000000000..61f292587b --- /dev/null +++ b/src/cli/evmCommands.ts @@ -0,0 +1,37 @@ +import { z } from "incur"; +import { resolve } from "node:path"; +import { createEvmInterfaceService } from "../composition/evm.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { logCliCommand } from "../cliLogging.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import type { Logger } from "../logger.js"; +import type { CliInstance } from "./types.js"; +import { withCommandCancellation } from "./commandCancellation.js"; + +/** Resolve operator paths and preserve explicit encoding through the shared workflow. */ +export const registerEvmCommands = ( + cli: CliInstance, + logger: Logger, + environment: Readonly, +): void => { + const service = createEvmInterfaceService(environment); + cli.command(CLI_COMMANDS.inspectEvmInterface, { + description: "Infer interface candidates from explicit local EVM bytecode", + args: z.object({ + path: z.string().describe("Local bytecode carrier"), + encoding: z + .enum(["raw", "hex"]) + .describe("Selected carrier representation"), + }), + run: ({ args }) => + withCommandCancellation((signal) => + logCliCommand(logger, CLI_COMMANDS.inspectEvmInterface, async () => { + const result = await service.inspect( + { path: resolve(args.path), encoding: args.encoding }, + { signal }, + ); + return result.ok ? result.value : projectAnalysisError(result.error); + }), + ), + }); +}; diff --git a/src/cli/firmwareCommands.ts b/src/cli/firmwareCommands.ts index 961b443860..ad08c7804b 100644 --- a/src/cli/firmwareCommands.ts +++ b/src/cli/firmwareCommands.ts @@ -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 } }), diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts index b1f08da9df..92d4696c46 100644 --- a/src/cli/javascriptApplicationAnalysis.ts +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -1,13 +1,33 @@ import { analyzeJavaScriptApplication } from "../application/javascript/JavaScriptApplicationService.js"; +import { createProgressReporter } from "../application/ProgressReporter.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { JsonValue } from "../domain/jsonValue.js"; +import type { CliCommandOutput } from "./streamedJsonOutput.js"; /** Execute the shared one-shot CLI boundary for static JavaScript analysis. */ export const runCliJavaScriptApplicationAnalysis = async ( input: unknown, -): Promise => { - const result = await analyzeJavaScriptApplication(input); - return result.ok ? result.value : cliError(result.error); + output?: CliCommandOutput, +): Promise => { + const started = performance.now(); + const progress = createProgressReporter( + async (update) => { + process.stderr.write(`${JSON.stringify({ rea_progress: update })}\n`); + }, + { minimumIntervalMs: 0 }, + ); + const result = await analyzeJavaScriptApplication(input, { progress }); + const value = result.ok ? result.value : cliError(result.error); + if ( + output !== undefined && + (await output.output.write(value, { + command: output.command, + format: output.format, + duration: `${Math.round(performance.now() - started)}ms`, + })) + ) + return undefined; + return value; }; const cliError = ( diff --git a/src/cli/jsonOutput.ts b/src/cli/jsonOutput.ts new file mode 100644 index 0000000000..cc401ee321 --- /dev/null +++ b/src/cli/jsonOutput.ts @@ -0,0 +1,153 @@ +import type { Writable } from "node:stream"; + +type JsonOutputFormat = "json" | "jsonl"; + +/** Write ordinary JSON without constructing a document-sized string. */ +export const writeJsonOutput = async ( + value: unknown, + destination: Writable, + format: JsonOutputFormat = "json", +): Promise => { + for (const part of bufferedParts(documentParts(value, format))) + await writePart(destination, part); +}; + +const writePart = (destination: Writable, part: string): Promise => + new Promise((resolve, reject) => { + const onError = (cause: unknown): void => reject(cause); + destination.once("error", onError); + try { + destination.write(part, (cause) => { + if (cause) { + // A Writable can emit its error after invoking the write callback. + setImmediate(() => destination.off("error", onError)); + reject(cause); + } else { + destination.off("error", onError); + resolve(); + } + }); + } catch (cause: unknown) { + destination.off("error", onError); + reject(cause); + } + }); + +function* documentParts( + value: unknown, + format: JsonOutputFormat, +): Generator { + if (format === "jsonl" && Array.isArray(value)) { + for (const item of value) { + yield* encodeValue(item, false, 0, new Set()); + yield "\n"; + } + return; + } + yield* encodeValue(value, format === "json", 0, new Set()); + yield "\n"; +} + +function* bufferedParts(parts: Iterable): Generator { + let buffer = ""; + for (const part of parts) { + if (buffer.length + part.length > 64 * 1024 && buffer !== "") { + yield buffer; + buffer = ""; + } + buffer += part; + } + if (buffer !== "") yield buffer; +} + +function* encodeString(value: string): Generator { + if (value.length <= 8192) { + yield JSON.stringify(value); + return; + } + yield '"'; + for (let start = 0; start < value.length;) { + let end = Math.min(start + 8192, value.length); + const last = value.charCodeAt(end - 1); + const next = value.charCodeAt(end); + // Keep a surrogate pair in the same JSON.stringify and UTF-8 write. + if ( + end < value.length && + last >= 0xd800 && + last <= 0xdbff && + next >= 0xdc00 && + next <= 0xdfff + ) + end -= 1; + yield JSON.stringify(value.slice(start, end)).slice(1, -1); + start = end; + } + yield '"'; +} + +function* encodeValue( + value: unknown, + pretty: boolean, + depth: number, + ancestors: Set, +): Generator { + if (typeof value === "string") { + yield* encodeString(value); + return; + } + if (value === null || typeof value === "boolean") { + yield value === null ? "null" : String(value); + return; + } + if (typeof value === "number" && Number.isFinite(value)) { + yield String(value); + return; + } + if (typeof value !== "object" || value === null) + throw new TypeError("CLI JSON output requires ordinary JSON values"); + if (ancestors.has(value)) + throw new TypeError("CLI JSON output contains a circular reference"); + if ( + !Array.isArray(value) && + Object.prototype.toString.call(value) !== "[object Object]" + ) + throw new TypeError("CLI JSON output requires ordinary JSON objects"); + ancestors.add(value); + try { + const array = Array.isArray(value); + const keys = array + ? undefined + : Object.keys(value).filter((key) => { + const item: unknown = Reflect.get(value, key); + // Incur's filter can select inherited methods. Match native JSON's + // omission of fields that have no JSON representation. + return ( + item !== undefined && + typeof item !== "function" && + typeof item !== "symbol" + ); + }); + const count = array ? value.length : (keys?.length ?? 0); + yield array ? "[" : "{"; + for (let index = 0; index < count; index += 1) { + if (index > 0) yield ","; + if (pretty) yield `\n${" ".repeat(depth + 1)}`; + let item: unknown; + if (array) { + item = value[index]; + } else { + const key = keys?.[index]; + if (key === undefined) + throw new TypeError("CLI JSON object changed during serialization"); + yield* encodeString(key); + yield pretty ? ": " : ":"; + item = Reflect.get(value, key); + } + yield* encodeValue(item, pretty, depth + 1, ancestors); + } + if (pretty && count > 0) yield `\n${" ".repeat(depth)}`; + yield array ? "]" : "}"; + } finally { + ancestors.delete(value); + } +} diff --git a/src/cli/nativeCallCommands.ts b/src/cli/nativeCallCommands.ts new file mode 100644 index 0000000000..8a796a2dfe --- /dev/null +++ b/src/cli/nativeCallCommands.ts @@ -0,0 +1,57 @@ +import { z } from "incur"; + +import { runProviderAnalysis } from "../composition/directAnalysis.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { parseCliJsonInput } from "../cliJsonInput.js"; +import { logCliCommand } from "../cliLogging.js"; +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { projectInputIssues } from "../domain/inputIssueProjection.js"; +import { jsonValueSchema } from "../domain/jsonValue.js"; +import { nativeCallObservationInputSchema } from "../domain/native/nativeCallObservation.js"; +import type { Logger } from "../logger.js"; +import type { CliInstance } from "./types.js"; + +const OPERATION = "observe_native_calls"; + +/** `observe-native-calls `: the MCP input object as JSON. */ +export const registerNativeCallCommands = ( + cli: CliInstance, + logger: Logger, +): void => { + cli.command(CLI_COMMANDS.observeNativeCalls, { + description: + "Launch a Mach-O under LLDB and record entries into selected functions or Objective-C methods", + args: z.object({ + path: z.string().describe("Mach-O or app path"), + inputJson: z + .string() + .describe( + "Inline JSON or a JSON file path: breakpoints, arguments, environment, duration_ms, max_events, argument_registers, backtrace_frames", + ), + }), + run: ({ args }) => + logCliCommand(logger, CLI_COMMANDS.observeNativeCalls, async () => { + const input = await parseCliJsonInput(args.inputJson, OPERATION); + if (!input.ok) return input.error; + const parsed = nativeCallObservationInputSchema.safeParse(input.value); + if (!parsed.success) + return { + error: "Analysis failed", + ...projectAnalysisError( + new AnalysisInputError( + OPERATION, + undefined, + projectInputIssues(parsed.error.issues, input.value), + ), + ), + }; + return runProviderAnalysis( + args.path, + OPERATION, + z.record(z.string(), jsonValueSchema).parse(parsed.data), + logger, + ); + }), + }); +}; diff --git a/src/cli/processCommands.ts b/src/cli/processCommands.ts index e68059b7c4..af10e95c82 100644 --- a/src/cli/processCommands.ts +++ b/src/cli/processCommands.ts @@ -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"; diff --git a/src/cli/streamedJsonOutput.ts b/src/cli/streamedJsonOutput.ts new file mode 100644 index 0000000000..282316d0ad --- /dev/null +++ b/src/cli/streamedJsonOutput.ts @@ -0,0 +1,84 @@ +import { type Writable } from "node:stream"; + +import { Filter } from "incur"; + +import { parseCliOutputArguments } from "../cliOutput.js"; +import type { JsonValue } from "../domain/jsonValue.js"; +import { writeJsonOutput } from "./jsonOutput.js"; + +/** Result metadata supplied by the command that actually ran. */ +export interface CliResultOutputMetadata { + readonly command: string; + readonly duration: string; + readonly format: string; +} + +/** Optional executable-owned result surface for one CLI invocation. */ +export interface CliResultOutput { + readonly handled: boolean; + readonly failed: boolean; + write(value: JsonValue, metadata: CliResultOutputMetadata): Promise; +} + +/** Selected command output context, independent of application workflows. */ +export interface CliCommandOutput { + readonly output: CliResultOutput; + readonly command: string; + readonly format: string; +} + +/** Preserve Incur's JSON controls while streaming the complete result. */ +export const createStreamedCliJsonOutput = ( + arguments_: readonly string[], + destination: Writable, +): CliResultOutput | undefined => { + const options = parseCliOutputArguments(arguments_); + if ( + options.parseError || + options.tokenCount || + options.tokenWindow || + (options.format !== "json" && options.format !== "jsonl") + ) + return undefined; + const format = options.format; + let handled = false; + let failed = false; + return { + get handled() { + return handled; + }, + get failed() { + return failed; + }, + async write(value, metadata) { + if (metadata.format !== format) return false; + const filtered: unknown = options.filterOutput + ? Filter.apply(value, Filter.parse(options.filterOutput)) + : value; + const meta = { + command: metadata.command, + duration: metadata.duration, + }; + const document = options.fullOutput + ? filtered === undefined + ? { ok: true, meta } + : { ok: true, data: filtered, meta } + : filtered === undefined || + filtered === null || + typeof filtered === "function" || + typeof filtered === "symbol" + ? {} + : filtered; + // Once writing starts, a failed destination must not receive a second + // document from Incur's error formatter. + handled = true; + try { + await writeJsonOutput(document, destination, format); + } catch (cause: unknown) { + failed = true; + throw cause; + } + return true; + }, + }; +}; diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts index 79a8a74334..ae31f4b410 100644 --- a/src/cli/utilityCommands.ts +++ b/src/cli/utilityCommands.ts @@ -1,5 +1,4 @@ import { z } from "incur"; -import { jsonValueSchema } from "../domain/jsonValue.js"; import { runCapabilityStatus, @@ -10,6 +9,10 @@ import { importReferenceSource } from "../application/ReferenceSourceImport.js"; import { projectReferenceSourceImportError } from "../application/ReferenceSourceImportTypes.js"; import { parseConfig } from "../config.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { projectInputIssues } from "../domain/inputIssueProjection.js"; +import { safeParseJson } from "../domain/safeJson.js"; +import { nativeUiScenarioInputSchema } from "../domain/native/nativeUiObservation.js"; import { PRODUCT_IDENTITY } from "../identity.js"; import { logCliCommand } from "../cliLogging.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -17,6 +20,7 @@ import { swiftSymbolsSchema } from "../contracts/native/nativeToolContracts.js"; import type { Logger } from "../logger.js"; import { isReferenceSourceImportCliFailure } from "./referenceSourceImportStatus.js"; import type { CliInstance } from "./types.js"; +import { registerNativeCallCommands } from "./nativeCallCommands.js"; export const registerUtilityCommands = ( cli: CliInstance, @@ -25,6 +29,7 @@ export const registerUtilityCommands = ( ): void => { registerCapabilityCommands(cli, logger); registerNativeCommands(cli, logger); + registerNativeCallCommands(cli, logger); for (const [command, operation] of [ [CLI_COMMANDS.observeNativeUi, "observe_native_ui"], [CLI_COMMANDS.captureNativeUiScenario, "capture_native_ui_scenario"], @@ -67,33 +72,59 @@ export const registerUtilityCommands = ( maxNodes: "max-nodes", }, run: ({ args, options }) => - logCliCommand(logger, command, () => - runProviderAnalysis( - args.path, - operation, - { - pid: options.pid, - window_id: options.windowId, - screenshot: options.screenshot, - accessibility: options.accessibility, - max_nodes: options.maxNodes, - ...(operation === "capture_native_ui_scenario" - ? { - steps: - options.steps === undefined - ? [] - : jsonValueSchema.parse(JSON.parse(options.steps)), - } - : {}), - }, - logger, - ), - ), + logCliCommand(logger, command, async () => { + const parameters = { + pid: options.pid, + window_id: options.windowId, + screenshot: options.screenshot, + accessibility: options.accessibility, + max_nodes: options.maxNodes, + }; + if (operation === "capture_native_ui_scenario") { + const decoded = + options.steps === undefined + ? { ok: true as const, value: undefined } + : safeParseJson(options.steps); + if (!decoded.ok) + return invalidNativeUiScenarioInput(operation, [ + { path: ["steps"], reason: "invalid_format", expected: "JSON" }, + ]); + const parsed = nativeUiScenarioInputSchema.safeParse({ + ...parameters, + steps: decoded.value, + }); + if (!parsed.success) + return invalidNativeUiScenarioInput( + operation, + projectInputIssues(parsed.error.issues, { + ...parameters, + ...(options.steps === undefined + ? {} + : { steps: decoded.value }), + }), + ); + return runProviderAnalysis( + args.path, + operation, + parsed.data, + logger, + ); + } + return runProviderAnalysis(args.path, operation, parameters, logger); + }), }); } registerReferenceSourceCommand(cli, logger, environment); }; +const invalidNativeUiScenarioInput = ( + operation: string, + issues: ConstructorParameters[2], +) => ({ + error: "Application workflow failed", + ...projectAnalysisError(new AnalysisInputError(operation, undefined, issues)), +}); + const registerCapabilityCommands = (cli: CliInstance, logger: Logger): void => { for (const command of [ CLI_COMMANDS.capabilities, diff --git a/src/cli/webNetworkCaptureCommands.ts b/src/cli/webNetworkCaptureCommands.ts new file mode 100644 index 0000000000..a5601b57bb --- /dev/null +++ b/src/cli/webNetworkCaptureCommands.ts @@ -0,0 +1,60 @@ +import { z } from "incur"; +import { createWebNetworkCaptureService } from "../composition/webNetworkCaptures.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { logCliCommand } from "../cliLogging.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import type { Logger } from "../logger.js"; +import type { CliInstance } from "./types.js"; +import { withCommandCancellation } from "./commandCancellation.js"; + +/** Expose offline historical capture inspection with the same selection contract as MCP. */ +export const registerWebNetworkCaptureCommands = ( + cli: CliInstance, + logger: Logger, + environment: Readonly, +): void => { + const service = createWebNetworkCaptureService(environment); + cli.command(CLI_COMMANDS.inspectWebNetworkCapture, { + description: + "Inspect retained HAR or native mitmproxy records without fetching recorded URLs", + args: z.object({ + capturePath: z.string().describe("Absolute local capture path"), + format: z.enum(["har", "mitmproxy"]).describe("Producer capture format"), + }), + options: z.object({ + record: z + .array(z.coerce.number().int().nonnegative()) + .optional() + .describe("Zero-based producer ordinals; repeat to select several"), + sensitiveValue: z + .array(z.string().min(1)) + .default([]) + .describe( + "Literal string values explicitly marked sensitive; repeat as needed", + ), + }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand( + logger, + CLI_COMMANDS.inspectWebNetworkCapture, + async () => { + const result = await service.inspect( + { + capture_path: args.capturePath, + format: args.format, + sensitive_values: options.sensitiveValue, + ...(options.record === undefined + ? {} + : { record_ordinals: options.record }), + }, + { signal }, + ); + return result.ok + ? result.value + : projectAnalysisError(result.error); + }, + ), + ), + }); +}; diff --git a/src/cliBrowserScenarioCommands.ts b/src/cliBrowserScenarioCommands.ts index 877eae8131..23984effbd 100644 --- a/src/cliBrowserScenarioCommands.ts +++ b/src/cliBrowserScenarioCommands.ts @@ -4,10 +4,11 @@ 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"; +import { projectInputIssues } from "./domain/inputIssueProjection.js"; import { browserScenarioSchema } from "./domain/browserScenario.js"; import type { JsonValue } from "./domain/jsonValue.js"; import type { Logger } from "./logger.js"; @@ -31,9 +32,17 @@ 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(OPERATION)); + return cliError( + new AnalysisInputError( + OPERATION, + { cause: scenario.error }, + projectInputIssues(scenario.error.issues, input.value), + ), + ); const result = await captureBrowserScenario( createBrowserScenarioProvider(), scenario.data, diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts index 9cc5cac8f2..61a85bb25a 100644 --- a/src/cliCommandNames.ts +++ b/src/cliCommandNames.ts @@ -1,5 +1,7 @@ /** Canonical names used by every one-shot CLI command registration. */ export const CLI_COMMANDS = Object.freeze({ + inspectEvmInterface: "inspect-evm-interface", + inspectWebNetworkCapture: "inspect-web-network-capture", observeWebExecution: "observe-web-execution", inspectWebEventListeners: "inspect-web-event-listeners", traceWebSourceLocation: "trace-web-source-location", @@ -27,6 +29,7 @@ export const CLI_COMMANDS = Object.freeze({ extractArtifact: "extract-artifact", decodeInterfaceBuilder: "decode-interface-builder", inspectKeyedArchive: "inspect-keyed-archive", + traceDylibResolution: "trace-dylib-resolution", inspectNativeDataType: "inspect-native-data-type", inspectNativeLoadImage: "inspect-native-load-image", readBytes: "read-bytes", @@ -36,6 +39,8 @@ export const CLI_COMMANDS = Object.freeze({ resolveNativeCallTargets: "resolve-native-call-targets", inspectAssetCatalog: "inspect-asset-catalog", inspectManagedArtifact: "inspect-managed-artifact", + inspectRecordedCrash: "inspect-recorded-crash", + inspectBinaryLayout: "inspect-binary-layout", inspectFirmwareRegions: "inspect-firmware-regions", extractFirmware: "extract-firmware", inspectAndroidPackage: "inspect-android-package", @@ -50,6 +55,7 @@ export const CLI_COMMANDS = Object.freeze({ importManagedReconstruction: "import-managed-reconstruction", projectManagedApplicationGraph: "project-managed-application-graph", observeNativeUi: "observe-native-ui", + observeNativeCalls: "observe-native-calls", captureNativeUiScenario: "capture-native-ui-scenario", inspectMacho: "inspect-macho", inspectSignature: "inspect-signature", diff --git a/src/cliEnvironmentSeam.test.ts b/src/cliEnvironmentSeam.test.ts index 4aa57dca39..6ad3008605 100644 --- a/src/cliEnvironmentSeam.test.ts +++ b/src/cliEnvironmentSeam.test.ts @@ -4,26 +4,13 @@ import { join } from "node:path"; 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", () => { - expect(createCli({ REA_LOG_LEVEL: "debug" })).toBeDefined(); - expect(createCli({})).toBeDefined(); - }); - - it("still builds with no environment supplied", () => { - // The default keeps production behaviour identical. - expect(createCli()).toBeDefined(); - expect(createSystemDoctorHost()).toBeDefined(); - }); - it("resolves from injected PATH and inherits injected env with scenario overrides", async ({ skip, }) => { diff --git a/src/cliHostInputs.test.ts b/src/cliHostInputs.test.ts index 8086f950d7..96c7cd0054 100644 --- a/src/cliHostInputs.test.ts +++ b/src/cliHostInputs.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vitest"; -import { browserContext } from "./cliBrowserContext.js"; -import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js"; import { HopperProvider } from "./hopper/HopperProvider.js"; import { silentLogger } from "./logger.js"; import { parseConfig } from "./config.js"; @@ -35,9 +33,3 @@ describe("host platform is injected, not read from the ambient process", () => { }, ); }); - -describe("browser CLI provider setup", () => { - it("does not require permission policy configuration", () => { - expect(browserContext().provider).toBeInstanceOf(CdpBrowserProvider); - }); -}); diff --git a/src/cliJsonInput.ts b/src/cliJsonInput.ts index 370a2f1b09..8bb2d105a5 100644 --- a/src/cliJsonInput.ts +++ b/src/cliJsonInput.ts @@ -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"; @@ -15,8 +16,6 @@ export const parseCliJsonInput = async ( > => { const inline = parseJson(value); if (inline !== undefined) return { ok: true, value: inline }; - if (["{", "["].includes(value.trimStart()[0] ?? "")) - return { ok: false, error: inputError(operation) }; try { // Read raw bytes so invalid UTF-8 is rejected by parseJson instead of // being silently replaced by lossy "utf8" decoding. @@ -25,10 +24,66 @@ export const parseCliJsonInput = async ( ? jsonFileError(value, operation, "invalid-json") : { ok: true, value: parsed }; } catch (cause: unknown) { + if ( + ["{", "["].includes(value.trimStart()[0] ?? "") && + cannotBeAnExistingFile(cause) && + !hasExplicitJsonFileExtension(value) + ) + return { ok: false, error: inputError(operation) }; return jsonFileError(value, operation, "read-failed", cause); } }; +/** + * 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 nonblank strings are resolved, everything else passes through + * untouched so shared schemas can reject blank paths. Shared MCP contracts reject + * relative paths, so CLI workflows resolve operator-relative values before validation. + */ +export const resolveCliJsonPaths = ( + value: unknown, + paths: ReadonlyArray>, +): unknown => { + let resolved = value; + for (const keys of paths) resolved = resolveJsonPath(resolved, keys); + return resolved; +}; + +const resolveJsonPath = ( + value: unknown, + keys: ReadonlyArray, +): 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" && entry.trim().length > 0 + ? 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 === "ENOTDIR"); + +const hasExplicitJsonFileExtension = (value: string): boolean => + value.toLowerCase().endsWith(".json"); + const parseJson = (value: string | Uint8Array): unknown => { let text: string; if (typeof value === "string") { diff --git a/src/cliJsonPaths.test.ts b/src/cliJsonPaths.test.ts new file mode 100644 index 0000000000..936bfacea1 --- /dev/null +++ b/src/cliJsonPaths.test.ts @@ -0,0 +1,59 @@ +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { resolveCliJsonPaths } from "./cliJsonInput.js"; +import { electronActiveObservationInputSchema } from "./domain/javascript/electronActiveObservation.js"; + +const paths = [ + ["executable_path"], + ["application_path"], + ["application_root"], +] as const; + +describe("CLI JSON path normalization", () => { + it.each([ + ["executable_path", ""], + ["executable_path", " \t "], + ["application_path", ""], + ["application_path", " \t "], + ["application_root", ""], + ["application_root", " \t "], + ])( + "keeps blank %s (%j) invalid at the shared input boundary", + (field, value) => { + const input = { + executable_path: "/synthetic/electron", + application_path: "/synthetic/app", + application_root: "/synthetic", + [field]: value, + }; + const parsed = electronActiveObservationInputSchema.safeParse( + resolveCliJsonPaths(input, paths), + ); + expect(parsed.success).toBe(false); + if (parsed.success) throw new Error("Expected blank path rejection"); + expect(parsed.error.issues).toEqual( + expect.arrayContaining([expect.objectContaining({ path: [field] })]), + ); + expect(input).toHaveProperty(field, value); + }, + ); + + it("resolves selected relative paths without trimming names or changing unrelated fields", () => { + const input = { + browser: { executable_path: " browser ", label: "relative-label" }, + application_path: "app", + }; + expect( + resolveCliJsonPaths(input, [["browser", "executable_path"]]), + ).toEqual({ + browser: { + executable_path: resolve(" browser "), + label: "relative-label", + }, + application_path: "app", + }); + expect(input.browser.executable_path).toBe(" browser "); + }); +}); diff --git a/src/cliLogging.test.ts b/src/cliLogging.test.ts index c02cb2041e..b37a6f183e 100644 --- a/src/cliLogging.test.ts +++ b/src/cliLogging.test.ts @@ -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([ diff --git a/src/cliObservationOptions.test.ts b/src/cliObservationOptions.test.ts deleted file mode 100644 index b6a7926dec..0000000000 --- a/src/cliObservationOptions.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { electronPageInspectionOptions } from "./cliObservationOptions.js"; - -describe("Electron CLI observation duration", () => { - it("accepts caller-selected windows longer than ten seconds", () => { - expect( - electronPageInspectionOptions.parse({ observationMs: 60_000 }) - .observationMs, - ).toBe(60_000); - }); -}); diff --git a/src/cliObservationOptions.ts b/src/cliObservationOptions.ts index 7d0bac6afc..14fa422150 100644 --- a/src/cliObservationOptions.ts +++ b/src/cliObservationOptions.ts @@ -1,4 +1,5 @@ import { z } from "incur"; +import { artifactIntegrityPolicySchema } from "./domain/artifactGraph.js"; /** Accept an agent-selected browser observation duration without an artificial ceiling. */ export const observationDuration = (fallback: number, minimum = 0) => @@ -63,8 +64,7 @@ export const javascriptApplicationOptions = z.object({ .enum(["auto", "asar", "directory"]) .default("auto") .describe("Application artifact format"), - integrityPolicy: z - .enum(["fail", "record-and-continue"]) - .default("fail") - .describe("Behavior when declared artifact integrity does not match"), + integrityPolicy: artifactIntegrityPolicySchema.describe( + "Behavior when declared artifact integrity does not match", + ), }); diff --git a/src/cliOutput.ts b/src/cliOutput.ts index 96f65a7e7b..a5b3599921 100644 --- a/src/cliOutput.ts +++ b/src/cliOutput.ts @@ -6,6 +6,7 @@ const UNSUPPORTED_OUTPUT_COMBINATION_MESSAGE = "Token windows cannot preserve structured output. Remove --token-limit/--token-offset and use --filter-output or command pagination."; type StructuredOutputFormat = "json" | "jsonl" | "yaml"; +const INCUR_OUTPUT_FORMATS = new Set(["toon", "json", "yaml", "md", "jsonl"]); export type CliOutputArgumentValidation = | { readonly ok: true } @@ -19,8 +20,11 @@ export type CliOutputArgumentValidation = type JsonRecord = Record; interface ParsedCliOutputArguments { - readonly filterOutput: boolean; + readonly filterOutput: string | undefined; readonly format: string; + readonly fullOutput: boolean; + readonly parseError: boolean; + readonly tokenCount: boolean; readonly tokenWindow: boolean; } @@ -34,12 +38,23 @@ const validationError = (value: JsonRecord): JsonRecord | undefined => { return undefined; }; -const parseCliOutputArguments = ( +/** Read effective Incur output controls at the executable boundary. */ +export const parseCliOutputArguments = ( arguments_: readonly string[], ): ParsedCliOutputArguments => { let format: string = "toon"; - let filterOutput = false; + let filterOutput: string | undefined; + let fullOutput = false; + let tokenCount = false; let tokenWindow = false; + const parsed = (parseError: boolean): ParsedCliOutputArguments => ({ + filterOutput, + format, + fullOutput, + parseError, + tokenCount, + tokenWindow: parseError ? false : tokenWindow, + }); for (let index = 0; index < arguments_.length; index += 1) { const argument = arguments_[index]; // Incur scans builtins across `--` and accepts only spaced value forms. @@ -50,40 +65,46 @@ const parseCliOutputArguments = ( } if (argument === "--format") { const value = arguments_[index + 1]; - if (value !== undefined) { + if (value) { + // Incur stops parsing at an invalid format before applying any later + // output controls. Let it report that malformed flag itself. + if (!INCUR_OUTPUT_FORMATS.has(value)) return parsed(true); format = value; index += 1; } continue; } if (argument === "--filter-output") { - if (arguments_[index + 1] !== undefined) { - filterOutput = true; + if (arguments_[index + 1]) { + filterOutput = arguments_[index + 1]; index += 1; } continue; } if (argument === "--token-limit" || argument === "--token-offset") { - if (arguments_[index + 1] !== undefined) { + const value = arguments_[index + 1]; + if (value) { + const numericValue = Number(value); + if (!Number.isFinite(numericValue) || value.trim() === "") + return parsed(true); tokenWindow = true; index += 1; } continue; } - if ( - argument?.startsWith("--token-limit=") || - argument?.startsWith("--token-offset=") - ) - tokenWindow = true; + if (argument === "--full-output") fullOutput = true; + if (argument === "--token-count") tokenCount = true; } - return { filterOutput, format, tokenWindow }; + return parsed(false); }; /** Reject text-window controls that would corrupt a structured document. */ export const validateCliOutputArguments = ( arguments_: readonly string[], ): CliOutputArgumentValidation => { - const { format, tokenWindow } = parseCliOutputArguments(arguments_); + const { format, parseError, tokenWindow } = + parseCliOutputArguments(arguments_); + if (parseError) return { ok: true }; if ( tokenWindow && (format === "json" || format === "jsonl" || format === "yaml") @@ -101,7 +122,9 @@ export const validateCliOutputArguments = ( export const renderEmptyFilteredCliOutput = ( arguments_: readonly string[], ): string | undefined => { - const { filterOutput, format } = parseCliOutputArguments(arguments_); + const { filterOutput, format, parseError } = + parseCliOutputArguments(arguments_); + if (parseError) return undefined; if ( filterOutput && (format === "json" || format === "jsonl" || format === "yaml") diff --git a/src/composition/binaryDiagnostics.ts b/src/composition/binaryDiagnostics.ts new file mode 100644 index 0000000000..7fab8d73a2 --- /dev/null +++ b/src/composition/binaryDiagnostics.ts @@ -0,0 +1,16 @@ +import { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js"; +import { PwntoolsRecordedCrashProvider } from "../native/pwntools/PwntoolsRecordedCrashProvider.js"; +import { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; +import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvider.js"; + +/** Compose the offline adapter without import-time I/O or dependency acquisition. */ +export const createBinaryLayoutService = ( + environment: Readonly = process.env, +): BinaryLayoutService => + new BinaryLayoutService(new PwntoolsLayoutProvider(environment)); + +/** Compose recorded-core inspection without acquiring a debugger or toolchain. */ +export const createRecordedCrashService = ( + environment: Readonly = process.env, +): RecordedCrashService => + new RecordedCrashService(new PwntoolsRecordedCrashProvider(environment)); diff --git a/src/composition/evm.ts b/src/composition/evm.ts new file mode 100644 index 0000000000..92c7e73fce --- /dev/null +++ b/src/composition/evm.ts @@ -0,0 +1,8 @@ +import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; +import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js"; + +/** Compose offline interface analysis without import-time I/O or dependency acquisition. */ +export const createEvmInterfaceService = ( + environment: Readonly = process.env, +): EvmInterfaceService => + new EvmInterfaceService(new EvmoleInterfaceProvider(environment)); diff --git a/src/composition/webNetworkCaptures.ts b/src/composition/webNetworkCaptures.ts new file mode 100644 index 0000000000..6dc7668d48 --- /dev/null +++ b/src/composition/webNetworkCaptures.ts @@ -0,0 +1,15 @@ +import { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js"; +import { HistoricalCaptureDecoder } from "../browser/history/HistoricalCaptureDecoder.js"; +import { HarCaptureAdapter } from "../browser/history/HarCaptureAdapter.js"; +import { MitmproxyCaptureAdapter } from "../browser/history/MitmproxyCaptureAdapter.js"; + +/** Compose offline format adapters without import-time reads, installation or process startup. */ +export const createWebNetworkCaptureService = ( + environment: Readonly = process.env, +): WebNetworkCaptureService => + new WebNetworkCaptureService( + new HistoricalCaptureDecoder( + [new HarCaptureAdapter(), new MitmproxyCaptureAdapter(environment)], + environment, + ), + ); diff --git a/src/config.test.ts b/src/config.test.ts index 42cef71724..303203c2f0 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { parseConfig } from "./config.js"; describe("runtime configuration", () => { - it("allows target-free startup and defaults to Hopper's documented launcher", () => { + it("allows target-free startup and applies runtime defaults", () => { const empty = parseConfig({}); expect(empty.ok).toBe(true); if (empty.ok) { @@ -13,11 +13,6 @@ describe("runtime configuration", () => { const result = parseConfig({ HOPPER_TARGET_PATH: "/usr/bin/true" }); expect(result.ok).toBe(true); if (result.ok) { - expect(result.value.hopperLauncherPath).toBe( - process.platform === "linux" - ? "/opt/hopper/bin/Hopper" - : "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper", - ); expect(result.value.hopperTargetKind).toBe("executable"); expect(result.value.hopperLoaderArgs).toEqual([]); expect(result.value.logLevel).toBe("info"); diff --git a/src/config/parseConfig.ts b/src/config/parseConfig.ts index 61a9152ba1..17fb465fb6 100644 --- a/src/config/parseConfig.ts +++ b/src/config/parseConfig.ts @@ -1,13 +1,36 @@ +import { accessSync, constants } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + import { ConfigurationError } from "../domain/configurationErrors.js"; import { ok, type Result } from "../domain/result.js"; import { parseEnvironment } from "./environment.js"; import { parseStringArray, parseLoaderArgs } from "./parsers.js"; import type { AppConfig } from "./types.js"; -const defaultHopperLauncherPath = (): string => - process.platform === "linux" - ? "/opt/hopper/bin/Hopper" - : "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper"; +const DEFAULT_HOPPER_LAUNCHER_PATH = + "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper"; +const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper"; + +const executableAvailable = (path: string): boolean => { + try { + accessSync(path, constants.X_OK); + return true; + } catch { + return false; + } +}; + +export const defaultHopperLauncherPath = ( + platform: NodeJS.Platform = process.platform, + homeDirectory: string = homedir(), + executable: (path: string) => boolean = executableAvailable, +): string => { + if (platform !== "linux") return DEFAULT_HOPPER_LAUNCHER_PATH; + if (executable(SYSTEM_LINUX_HOPPER)) return SYSTEM_LINUX_HOPPER; + const userLocal = join(homeDirectory, ".local/share/rea/hopper/bin/Hopper"); + return executable(userLocal) ? userLocal : SYSTEM_LINUX_HOPPER; +}; /** Parse provider configuration and runtime prerequisites at the composition root. */ export const parseConfig = ( diff --git a/src/contracts/artifactToolContracts.ts b/src/contracts/artifactToolContracts.ts index 3816020ebc..4db0fc8fdd 100644 --- a/src/contracts/artifactToolContracts.ts +++ b/src/contracts/artifactToolContracts.ts @@ -6,6 +6,7 @@ import { jsonValueSchema } from "../domain/jsonValue.js"; import { toolContractMetadata } from "./toolEffects.js"; import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js"; import { appleAssetCatalogInputSchema } from "../domain/apple/appleAssetCatalog.js"; +import { dylibResolutionInputSchema } from "../domain/apple/dylibResolution.js"; import { keyedArchiveInputSchema } from "../domain/apple/keyedArchive.js"; import { artifactIntegrityPolicySchema } from "../domain/artifactGraph.js"; /** Exact caller boundary for deterministic artifact inventory. */ @@ -31,6 +32,10 @@ const examples: Readonly>>> = { decode_interface_builder: {}, inspect_asset_catalog: { offset: 0, limit: 1000 }, inspect_keyed_archive: { path: "Contents/Resources/Model.plist" }, + trace_dylib_resolution: { + roots: ["Contents/MacOS/App"], + architecture: "arm64", + }, }; const artifact = < @@ -89,6 +94,11 @@ export const ARTIFACT_TOOL_CONTRACTS = [ "Inspect compiled Assets.car metadata in an active Apple app bundle. Returns stable paginated catalog and rendition records with raw assetutil metadata and catalog digests; rendition bytes are not extracted.", appleAssetCatalogInputSchema, ), + artifact( + "trace_dylib_resolution", + "Trace how dyld would resolve each Mach-O dylib load for the active Mach-O, or for every executable in the active .app bundle (the main app, XPC services, extensions, login items, helpers). Expands @executable_path, @loader_path, and @rpath through each process's LC_RPATH stack in dependents-first load order, and reports every candidate path with its outcome, the resolved image and digest, weak, re-export, and upward flags, and derived findings such as unresolved required loads or absent earlier @rpath candidates. Pure header parsing: nothing is executed, and absolute or system paths outside the analyzed root stay undetermined. Select roots or one architecture to narrow large bundles.", + dylibResolutionInputSchema, + ), ] as const satisfies readonly ToolContract[]; /** Provider operations; inventory remains an internal primitive of inspection. */ diff --git a/src/contracts/browserCaptureToolInputSchema.ts b/src/contracts/browserCaptureToolInputSchema.ts new file mode 100644 index 0000000000..bdc7dc1483 --- /dev/null +++ b/src/contracts/browserCaptureToolInputSchema.ts @@ -0,0 +1,62 @@ +import { browserCaptureComparisonInputSchema } from "../domain/browserCaptureComparison.js"; +import { browserScenarioCaptureSchema } from "../domain/browserScenarioCapture.js"; +import { captureSnapshotSchema } from "../domain/webCaptureDiffSchemas.js"; + +// Captures are complete producer results to pass back unchanged, rather than +// analyst-authored options. Describe that boundary without embedding the entire +// inspection result schema in every comparison input. The cloned parser still +// validates every nested field using the canonical domain schemas. +const passiveCaptureInputSchema = captureSnapshotSchema.clone( + captureSnapshotSchema.def, +); +passiveCaptureInputSchema._zod.toJSONSchema = () => ({ + type: "object", + properties: { + inspection: { + type: "object", + description: + "Complete result object returned by inspect_web_page. Pass the entire result unchanged, including capture identity, observations, completeness, and limitations. Every nested field is validated by REA.", + }, + webmcp: { + anyOf: [{ type: "object" }, { type: "null" }], + default: null, + description: + "Complete result object returned by discover_webmcp_tools for this capture, or null when it was not recorded. Pass the entire result unchanged. Every nested field is validated by REA.", + }, + }, + required: ["inspection"], +}); + +const scenarioCaptureInputSchema = browserScenarioCaptureSchema.clone( + browserScenarioCaptureSchema.def, +); +scenarioCaptureInputSchema._zod.toJSONSchema = () => ({ + type: "object", + description: + "Complete result object returned by capture_browser_scenario. Pass the entire result unchanged, including browser and scenario identity, steps, events, artifact contents and digests, completeness, and limitations. Every nested field is validated by REA.", +}); + +/** Advertise capture round trips while retaining full runtime validation. */ +export const browserCaptureToolInputSchema = + browserCaptureComparisonInputSchema.safeExtend({ + before: passiveCaptureInputSchema + .optional() + .describe( + "Earlier passive web-page capture with complete producer results.", + ), + after: passiveCaptureInputSchema + .optional() + .describe( + "Later passive web-page capture with complete producer results.", + ), + before_scenario: scenarioCaptureInputSchema + .optional() + .describe( + "Earlier complete result object returned by capture_browser_scenario.", + ), + after_scenario: scenarioCaptureInputSchema + .optional() + .describe( + "Later complete result object returned by capture_browser_scenario.", + ), + }); diff --git a/src/contracts/browserToolContracts.ts b/src/contracts/browserToolContracts.ts index ea16ce099f..445d52e81d 100644 --- a/src/contracts/browserToolContracts.ts +++ b/src/contracts/browserToolContracts.ts @@ -17,10 +17,7 @@ import { discoverWebMcpToolsInputSchema, webMcpDiscoverySchema, } from "../domain/webMcpDiscovery.js"; -import { - browserCaptureComparisonInputSchema, - browserCaptureComparisonSchema, -} from "../domain/browserCaptureComparison.js"; +import { browserCaptureComparisonSchema } from "../domain/browserCaptureComparison.js"; import { captureWebScreenshotInputSchema, compareWebScreenshotsInputSchema, @@ -29,6 +26,7 @@ import { } from "../domain/webScreenshot.js"; import { toolContractMetadata } from "./toolEffects.js"; import { evidenceResultOf } from "./toolOutputSchemas.js"; +import { browserCaptureToolInputSchema } from "./browserCaptureToolInputSchema.js"; const evidenceResult = evidenceResultOf; const listOutputSchema = evidenceResult(browserTargetListSchema); @@ -215,9 +213,9 @@ export const BROWSER_TOOL_CONTRACTS = [ name: "compare_web_captures", ...toolContractMetadata("compare_web_captures"), description: - "Compare passive web captures by providing before and after, or compare recorded scenarios by providing before_scenario and after_scenario with an optional normalization policy. Use exactly one input group. Scenario comparison aligns exact step IDs, records deterministic literal normalization, and returns every artifact-level difference plus alignment failures inline. Missing or truncated evidence never proves equality.", + "Compare passive web captures by providing before and after, each with the complete inspect_web_page result in inspection and an optional complete discover_webmcp_tools result in webmcp; or compare recorded scenarios by passing complete capture_browser_scenario results in before_scenario and after_scenario with an optional normalization policy. Use exactly one input group. Scenario comparison aligns exact step IDs, records deterministic literal normalization, and returns every artifact-level difference plus alignment failures inline. Missing or truncated evidence never proves equality.", kind: "browser-provider", - inputSchema: browserCaptureComparisonInputSchema, + inputSchema: browserCaptureToolInputSchema, outputSchema: captureDiffOutputSchema, examples: [ { diff --git a/src/contracts/contractSnapshot.test.ts b/src/contracts/contractSnapshot.test.ts index 1de10c515e..f0e7a6ce94 100644 --- a/src/contracts/contractSnapshot.test.ts +++ b/src/contracts/contractSnapshot.test.ts @@ -1,3 +1,5 @@ +import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js"; +import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js"; import { describe, expect, it } from "vitest"; import { z } from "zod"; @@ -16,6 +18,7 @@ import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js"; import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/javascriptRuntimeObservationToolContracts.js"; import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js"; import { WEB_RUNTIME_TOOL_CONTRACTS } from "./webRuntimeToolContracts.js"; +import { WEB_NETWORK_CAPTURE_TOOL_CONTRACTS } from "./webNetworkCaptureToolContracts.js"; import { WEB_SCRIPT_TOOL_CONTRACTS } from "./webScriptToolContracts.js"; import { JAVASCRIPT_RECOVERY_TOOL_CONTRACTS } from "./javascript/javascriptRecoveryToolContracts.js"; import { TOOL_EFFECTS } from "./toolEffects.js"; @@ -68,7 +71,9 @@ describe("tool contract surface", () => { ...OFFICIAL_TOOL_CONTRACTS, ...ENHANCED_TOOL_CONTRACTS, ...NATIVE_TOOL_CONTRACTS, + ...BINARY_DIAGNOSTICS_TOOL_CONTRACTS, ...ARTIFACT_TOOL_CONTRACTS, + ...EVM_TOOL_CONTRACTS, ...ANDROID_TOOL_CONTRACTS, ...FIRMWARE_TOOL_CONTRACTS, ...MANAGED_TOOL_CONTRACTS, @@ -80,6 +85,7 @@ describe("tool contract surface", () => { ...APPLICATION_TOOL_CONTRACTS, ...WEB_SCRIPT_TOOL_CONTRACTS, ...WEB_RUNTIME_TOOL_CONTRACTS, + ...WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, ...JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, ...SESSION_TOOL_CONTRACTS, ].map(({ name }) => name); diff --git a/src/contracts/errorSchemas.ts b/src/contracts/errorSchemas.ts index 7da9c19160..a535f6abf2 100644 --- a/src/contracts/errorSchemas.ts +++ b/src/contracts/errorSchemas.ts @@ -1,12 +1,16 @@ import { z } from "zod"; +import { jsonValueSchema } from "../domain/jsonValue.js"; + const categorySchema = z.enum([ "invalid_input", "unsupported_provider", + "unsupported_target", "integrity_mismatch", "truncated", "cancelled", "timeout", + "resource_constraint", "unavailable", "execution_failure", ]); @@ -21,7 +25,7 @@ const common = { retryable: z.boolean(), remediation: remediationSchema, }; -const genericDetails = z.record(z.string(), z.json()).optional(); +const genericDetails = z.record(z.string(), jsonValueSchema).optional(); const generic = (code: Code) => z .object({ code: z.literal(code), ...common, details: genericDetails }) @@ -30,10 +34,14 @@ const generic = (code: Code) => /** Stable discriminated schema shared by every CLI and MCP error surface. */ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [ generic("invalid_request"), + generic("access_denied"), + generic("artifact_changed"), generic("unreadable_output"), generic("capability_unavailable"), + generic("unsupported_target"), generic("provider_unavailable"), generic("provider_timeout"), + generic("resource_constraint"), generic("cancelled"), z .object({ diff --git a/src/contracts/evm/evmToolContracts.ts b/src/contracts/evm/evmToolContracts.ts new file mode 100644 index 0000000000..7b8f5bcf9f --- /dev/null +++ b/src/contracts/evm/evmToolContracts.ts @@ -0,0 +1,26 @@ +import { + inspectEvmInterfaceInputSchema, + evmInterfaceSchema, +} from "../../domain/evm/evmInterface.js"; +import type { ToolContract } from "../toolContractTypes.js"; +import { toolContractMetadata } from "../toolEffects.js"; +import { evidenceResultOf } from "../toolOutputSchemas.js"; + +/** Target-free EVM capabilities are independent of archive/Apple artifact providers. */ +export const EVM_TOOL_CONTRACTS = [ + { + name: "inspect_evm_interface", + ...toolContractMetadata("inspect_evm_interface"), + kind: "artifact-provider", + description: + "Inspect an explicit local flat EVM bytecode carrier and infer dispatch selectors, argument strings and mutability without executing the target or accessing a chain/RPC endpoint. Choose raw or hex encoding explicitly. Returns original carrier and decoded-byte SHA-256, full bytecode and interface candidates inline as inferred Evidence; bytecode kind, hardfork, deployed authenticity and discovery completeness remain unknown. Initial real-verified Linux x64 profile uses unchanged bundled EVMole 0.9.3/WASM in a bounded owned worker; EOF-style EF00 containers are unsupported. No signature lookup, wallet action or transaction broadcast.", + inputSchema: inspectEvmInterfaceInputSchema, + outputSchema: evidenceResultOf(evmInterfaceSchema), + examples: [ + { + title: "Inspect a local runtime bytecode hex carrier", + input: { path: "/artifacts/runtime.hex", encoding: "hex" }, + }, + ], + }, +] as const satisfies readonly ToolContract[]; diff --git a/src/contracts/investigationExamples.ts b/src/contracts/investigationExamples.ts index 72a61f721a..752f58742b 100644 --- a/src/contracts/investigationExamples.ts +++ b/src/contracts/investigationExamples.ts @@ -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, diff --git a/src/contracts/managed/managedToolContracts.ts b/src/contracts/managed/managedToolContracts.ts index dc57023227..85b5c19506 100644 --- a/src/contracts/managed/managedToolContracts.ts +++ b/src/contracts/managed/managedToolContracts.ts @@ -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; diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts new file mode 100644 index 0000000000..28d199a8b8 --- /dev/null +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -0,0 +1,52 @@ +import { + inspectRecordedCrashInputSchema, + recordedCrashSchema, +} from "../../domain/native/recordedCrash.js"; +import { + inspectBinaryLayoutInputSchema, + binaryLayoutSchema, +} from "../../domain/native/binaryLayout.js"; +import type { ToolContract } from "../toolContractTypes.js"; +import { toolContractMetadata } from "../toolEffects.js"; +import { evidenceResultOf } from "../toolOutputSchemas.js"; + +/** Offline binary diagnostics are independent of macOS process/UI providers. */ +export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [ + { + name: "inspect_binary_layout", + ...toolContractMetadata("inspect_binary_layout"), + kind: "native-provider", + description: + "Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Initial profile: ELF64 x86-64 little-endian EXEC/DYN/REL on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success.", + inputSchema: inspectBinaryLayoutInputSchema, + outputSchema: evidenceResultOf(binaryLayoutSchema), + examples: [ + { + title: "Inspect ELF linked and file layout", + input: { path: "/artifacts/application.elf" }, + }, + ], + }, + { + name: "inspect_recorded_crash", + ...toolContractMetadata("inspect_recorded_crash"), + kind: "native-provider", + description: + "Inspect a supplied Linux ELF64 amd64 little-endian core without launching its target or attaching to any live PID. Returns all recorded segments, raw note owners/descriptors and source ranges, every Linux CORE PRSTATUS thread/register and SIGINFO fields inline with SHA-256 Evidence. Historical PID is recorded metadata; current process/file identity, note coverage and signal-thread associations stay unknown. Optionally add derived core-only GDB/pwndbg mapping candidates through explicit BYO configuration; zero map flags mean unknown permissions. Uses the same stable snapshot for both stages, bounded complete output and owned cleanup. Initial Linux x64 profile: unchanged pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2; optional unchanged pwndbg 2026.09.15. No implicit executable/library pairing, GDB command language, host configuration changes or dependency installation.", + inputSchema: inspectRecordedCrashInputSchema, + outputSchema: evidenceResultOf(recordedCrashSchema), + examples: [ + { + title: "Inspect recorded registers and signal evidence", + input: { path: "/artifacts/crash.core" }, + }, + { + title: "Add core-only debugger mapping candidates", + input: { + path: "/artifacts/crash.core", + include_debugger_context: true, + }, + }, + ], + }, +] as const satisfies readonly ToolContract[]; diff --git a/src/contracts/native/nativeToolContracts.ts b/src/contracts/native/nativeToolContracts.ts index 1e3785d4c0..afeb9f652a 100644 --- a/src/contracts/native/nativeToolContracts.ts +++ b/src/contracts/native/nativeToolContracts.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { nativeCallObservationInputSchema } from "../../domain/native/nativeCallObservation.js"; import { nativeUiObservationInputSchema, nativeUiScenarioInputSchema, @@ -22,6 +23,19 @@ const examples: Readonly>>> = { steps: [{ kind: "wait", milliseconds: 100 }], }, demangle_swift: { symbols: ["$s4Test3fooyyF"] }, + observe_native_calls: { + breakpoints: [ + { + kind: "objc-method", + class_name: "NSURLSession", + selector: "dataTaskWithRequest:completionHandler:", + }, + { kind: "function", name: "open", module: "libsystem_kernel.dylib" }, + ], + arguments: ["--help"], + duration_ms: 5000, + backtrace_frames: 4, + }, }; const native = ( @@ -30,12 +44,13 @@ const native = ( 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 +60,7 @@ const native = ( .parse(examples[name] ?? {}), }, ], - } satisfies ToolContract; + } satisfies ToolContract; }; /** Ordered single-line symbols; `swift-demangle` ends each result with LF. */ @@ -70,6 +85,11 @@ export const NATIVE_TOOL_CONTRACTS = [ "Run selected-element press, increment/decrement scroll, AXValue text entry and bounded wait steps in one exact native app window. Returns ordered before/after captures and action/capture failures. Actions can change app data, network activity and persistent state; application state is left as-is and restoration is not attempted.", nativeUiScenarioInputSchema, ), + native( + "observe_native_calls", + "Launch the active Mach-O as an owned process under LLDB and record each entry into caller-selected functions or Objective-C methods: thread, module, symbol, load and file address, raw argument registers, selector, receiver class and optional caller frames. Every hit auto-continues. No expression is evaluated and no other process is attached. The process is killed when max_events or duration_ms is reached; exit status, captured stdout/stderr, unresolved breakpoints and limitations are returned inline. The process runs with the current user's permissions and may change files, show UI or use the network. A hardened-runtime target needs the get-task-allow entitlement.", + nativeCallObservationInputSchema, + ), native( "inspect_macho", "Inspect Mach-O slices, load commands, imports, exports, dependencies, build metadata, segments, sections, permissions, and exact command provenance without launching Hopper.", diff --git a/src/contracts/officialToolContracts.ts b/src/contracts/officialToolContracts.ts index 45efe5072d..6b69c80533 100644 --- a/src/contracts/officialToolContracts.ts +++ b/src/contracts/officialToolContracts.ts @@ -24,16 +24,18 @@ const official = ( 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; + examples: examplesFor(name, strictInputSchema), + } satisfies ToolContract; +}; /** Bridge operations exposed without additional application composition. */ export const OFFICIAL_TOOL_CONTRACTS = [ diff --git a/src/contracts/process/processCaptureExample.ts b/src/contracts/process/processCaptureExample.ts new file mode 100644 index 0000000000..3288fdac1d --- /dev/null +++ b/src/contracts/process/processCaptureExample.ts @@ -0,0 +1,2 @@ +/** Minimal valid process capture retained as a public contract example. */ +export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../domain/process/processCaptureExample.js"; diff --git a/src/contracts/processCaptureExample.ts b/src/contracts/processCaptureExample.ts deleted file mode 100644 index 5aed2939c6..0000000000 --- a/src/contracts/processCaptureExample.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Minimal valid process capture retained as a public contract example. */ -export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCaptureExample.js"; diff --git a/src/contracts/sessionLifecycleInputs.ts b/src/contracts/sessionLifecycleInputs.ts index fd0a1d58db..68b61b35fa 100644 --- a/src/contracts/sessionLifecycleInputs.ts +++ b/src/contracts/sessionLifecycleInputs.ts @@ -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), }); diff --git a/src/contracts/sessionPathInputs.test.ts b/src/contracts/sessionPathInputs.test.ts new file mode 100644 index 0000000000..6f41558244 --- /dev/null +++ b/src/contracts/sessionPathInputs.test.ts @@ -0,0 +1,167 @@ +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"; + +type PathCase = { + readonly name: string; + readonly accepts: (value: unknown) => boolean; + readonly relative: unknown[]; + readonly absolute: string; + readonly omitted?: () => boolean; +}; + +const cases: readonly PathCase[] = [ + { + name: "open binary snapshot", + accepts: (snapshot_path) => + openBinaryInputSchema.safeParse({ path: "/tmp/fixture", snapshot_path }) + .success, + relative: ["relative/analysis.json", "../outside/analysis.json"], + absolute: "/tmp/rea/analysis.json", + omitted: () => + openBinaryInputSchema.safeParse({ path: "/tmp/fixture" }).success, + }, + { + name: "close binary snapshot", + accepts: (snapshot_path) => + closeBinaryInputSchema.safeParse({ snapshot_path }).success, + relative: ["analysis.json"], + absolute: "/tmp/analysis.json", + omitted: () => closeBinaryInputSchema.safeParse({}).success, + }, + { + name: "evidence bundle export", + accepts: (path) => + exportEvidenceBundleInputSchema.safeParse({ path }).success, + relative: ["out/bundle.json", "../../bundle.json"], + absolute: "/tmp/bundle.json", + }, + { + name: "managed target", + accepts: (path) => managedArtifactInputSchema.safeParse({ path }).success, + relative: ["bin/app.dll"], + absolute: "/tmp/app.dll", + omitted: () => managedArtifactInputSchema.safeParse({}).success, + }, + { + name: "open binary target", + accepts: (path) => openBinaryInputSchema.safeParse({ path }).success, + relative: ["fixtures/app.bin"], + absolute: "/tmp/fixture.bin", + }, + { + name: "evidence bundle import", + accepts: (path) => + importEvidenceBundleInputSchema.safeParse({ path }).success, + relative: ["evidence.json"], + absolute: "/tmp/evidence.json", + }, + { + name: "firmware inspection", + accepts: (path) => + firmwareInputSchemas.inspect_firmware_regions.safeParse({ path }).success, + relative: ["firmware.bin"], + absolute: "/tmp/firmware.bin", + }, + { + name: "firmware extraction", + accepts: (path) => + firmwareInputSchemas.extract_firmware.safeParse({ + path, + output_directory: "/tmp/firmware-output", + }).success, + relative: ["firmware.bin"], + absolute: "/tmp/firmware.bin", + }, + { + name: "browser executable", + accepts: (executable_path) => + browserScenarioBrowserSchema.safeParse({ + mode: "launch", + executable_path, + }).success, + relative: ["chrome"], + absolute: "/opt/chromium/chrome", + }, + { + name: "Electron executable", + accepts: (executable_path) => + electronActiveObservationInputSchema.safeParse({ + executable_path, + application_path: "/tmp/electron-app/main.js", + }).success, + relative: ["Electron"], + absolute: "/Applications/Electron.app/Contents/MacOS/Electron", + }, + { + name: "Electron application", + accepts: (application_path) => + electronActiveObservationInputSchema.safeParse({ + executable_path: "/Applications/Electron.app/Contents/MacOS/Electron", + application_path, + }).success, + relative: ["main.js"], + absolute: "/tmp/electron-app/main.js", + }, + { + name: "Electron application root", + accepts: (application_root) => + electronActiveObservationInputSchema.safeParse({ + executable_path: "/Applications/Electron.app/Contents/MacOS/Electron", + application_path: "/tmp/electron-app/main.js", + application_root, + }).success, + relative: ["app"], + absolute: "/tmp/electron-app", + omitted: () => + electronActiveObservationInputSchema.safeParse({ + executable_path: "/Applications/Electron.app/Contents/MacOS/Electron", + application_path: "/tmp/electron-app/main.js", + }).success, + }, + { + name: "expected server", + accepts: (expected_server_path) => + binarySessionInputSchema.safeParse({ expected_server_path }).success, + relative: ["dist/main.js"], + absolute: "/opt/rea/dist/main.js", + omitted: () => binarySessionInputSchema.safeParse({}).success, + }, +]; + +it.each(cases)("$name requires an absolute local path", (pathCase) => { + for (const relative of pathCase.relative) + expect(pathCase.accepts(relative), pathCase.name).toBe(false); + expect(pathCase.accepts(pathCase.absolute), pathCase.name).toBe(true); + if (pathCase.omitted !== undefined) + expect(pathCase.omitted(), pathCase.name).toBe(true); +}); + +it("keeps evidence bundle export overwrite disabled by default", () => { + expect( + exportEvidenceBundleInputSchema.safeParse({ path: "/tmp/bundle.json" }), + ).toMatchObject({ success: true, data: { overwrite: false } }); +}); + +describe.runIf(process.platform === "win32")( + "Windows absolute path forms", + () => { + it.each([ + [closeBinaryInputSchema, { snapshot_path: "C:\\rea\\analysis.json" }], + [closeBinaryInputSchema, { snapshot_path: "C:/rea/analysis.json" }], + [exportEvidenceBundleInputSchema, { path: "C:/rea/bundle.json" }], + ])("accepts %o", (schema, input) => { + expect(schema.safeParse(input).success).toBe(true); + }); + }, +); diff --git a/src/contracts/sessionStatusContract.ts b/src/contracts/sessionStatusContract.ts index c19c73a3b9..303034c9c1 100644 --- a/src/contracts/sessionStatusContract.ts +++ b/src/contracts/sessionStatusContract.ts @@ -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(), }); diff --git a/src/contracts/sessionToolContracts.ts b/src/contracts/sessionToolContracts.ts index 81ce1836c8..ebd7983bd1 100644 --- a/src/contracts/sessionToolContracts.ts +++ b/src/contracts/sessionToolContracts.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { isAbsoluteLocalPath } from "../domain/localPath.js"; import { sessionOutputSchemas, requireOutputSchema, @@ -47,7 +48,16 @@ const session = ( /** 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), }); diff --git a/src/contracts/sessionToolSchemas.ts b/src/contracts/sessionToolSchemas.ts index 390709974f..87afe287ab 100644 --- a/src/contracts/sessionToolSchemas.ts +++ b/src/contracts/sessionToolSchemas.ts @@ -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. */ diff --git a/src/contracts/toolContractExamples.ts b/src/contracts/toolContractExamples.ts index 6a47936e22..2135a85432 100644 --- a/src/contracts/toolContractExamples.ts +++ b/src/contracts/toolContractExamples.ts @@ -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", diff --git a/src/contracts/toolContracts.test.ts b/src/contracts/toolContracts.test.ts index 6fec3a226c..f0086cff4b 100644 --- a/src/contracts/toolContracts.test.ts +++ b/src/contracts/toolContracts.test.ts @@ -1,3 +1,5 @@ +import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js"; +import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js"; import { describe, expect, it } from "vitest"; import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js"; @@ -17,12 +19,15 @@ import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js"; import { JAVASCRIPT_RECOVERY_TOOL_CONTRACTS } from "./javascript/javascriptRecoveryToolContracts.js"; import { WEB_SCRIPT_TOOL_CONTRACTS } from "./webScriptToolContracts.js"; import { WEB_RUNTIME_TOOL_CONTRACTS } from "./webRuntimeToolContracts.js"; +import { WEB_NETWORK_CAPTURE_TOOL_CONTRACTS } from "./webNetworkCaptureToolContracts.js"; const GROUPS = { official: OFFICIAL_TOOL_CONTRACTS, enhanced: ENHANCED_TOOL_CONTRACTS, native: NATIVE_TOOL_CONTRACTS, + binary_diagnostics: BINARY_DIAGNOSTICS_TOOL_CONTRACTS, artifact: ARTIFACT_TOOL_CONTRACTS, + evm: EVM_TOOL_CONTRACTS, managed: MANAGED_TOOL_CONTRACTS, android: ANDROID_TOOL_CONTRACTS, firmware: FIRMWARE_TOOL_CONTRACTS, @@ -34,6 +39,7 @@ const GROUPS = { javascript_recovery: JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, web_script_export: WEB_SCRIPT_TOOL_CONTRACTS, web_runtime: WEB_RUNTIME_TOOL_CONTRACTS, + web_network_capture: WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, session: SESSION_TOOL_CONTRACTS, } as const; diff --git a/src/contracts/toolContracts.ts b/src/contracts/toolContracts.ts index dd3896c437..304ef41d5f 100644 --- a/src/contracts/toolContracts.ts +++ b/src/contracts/toolContracts.ts @@ -1,3 +1,5 @@ +import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js"; +import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js"; import { NATIVE_TOOL_CONTRACTS } from "./native/nativeToolContracts.js"; import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js"; import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js"; @@ -10,6 +12,7 @@ import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/java import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js"; import { WEB_SCRIPT_TOOL_CONTRACTS } from "./webScriptToolContracts.js"; import { WEB_RUNTIME_TOOL_CONTRACTS } from "./webRuntimeToolContracts.js"; +import { WEB_NETWORK_CAPTURE_TOOL_CONTRACTS } from "./webNetworkCaptureToolContracts.js"; import { JAVASCRIPT_RECOVERY_TOOL_CONTRACTS } from "./javascript/javascriptRecoveryToolContracts.js"; import { OFFICIAL_TOOL_CONTRACTS } from "./officialToolContracts.js"; import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js"; @@ -22,7 +25,9 @@ export const TOOL_CONTRACTS = [ ...OFFICIAL_TOOL_CONTRACTS, ...ENHANCED_TOOL_CONTRACTS, ...NATIVE_TOOL_CONTRACTS, + ...BINARY_DIAGNOSTICS_TOOL_CONTRACTS, ...ARTIFACT_TOOL_CONTRACTS, + ...EVM_TOOL_CONTRACTS, ...MANAGED_TOOL_CONTRACTS, ...ANDROID_TOOL_CONTRACTS, ...FIRMWARE_TOOL_CONTRACTS, @@ -33,6 +38,7 @@ export const TOOL_CONTRACTS = [ ...APPLICATION_TOOL_CONTRACTS, ...WEB_SCRIPT_TOOL_CONTRACTS, ...WEB_RUNTIME_TOOL_CONTRACTS, + ...WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, ...JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, ...SESSION_TOOL_CONTRACTS, ] as const; diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index f1547b9795..558f4551fa 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -41,6 +41,16 @@ const sessionEvidence = effects({ mutatesSession: true }); /** Explicit effect audit for every public tool. */ export const TOOL_EFFECTS: Readonly> = { + inspect_evm_interface: effects({ + mutatesSession: true, + writesFilesystem: true, + launchesProcess: true, + }), + inspect_web_network_capture: effects({ + mutatesSession: true, + writesFilesystem: true, + launchesProcess: true, + }), observe_web_execution: effects({ mutatesTarget: true, mutatesSession: true, @@ -60,6 +70,16 @@ export const TOOL_EFFECTS: Readonly> = { launchesProcess: true, accessesNetwork: true, }), + inspect_recorded_crash: effects({ + mutatesSession: true, + writesFilesystem: true, + launchesProcess: true, + }), + inspect_binary_layout: effects({ + mutatesSession: true, + writesFilesystem: true, + launchesProcess: true, + }), inspect_firmware_regions: effects({ mutatesSession: true, launchesProcess: true, @@ -175,6 +195,15 @@ export const TOOL_EFFECTS: Readonly> = { mayDiscardData: true, idempotent: false, }), + // The owned process can change files, show UI and use the network. + observe_native_calls: effects({ + mutatesSession: true, + writesFilesystem: true, + launchesProcess: true, + accessesNetwork: true, + changesUiState: true, + idempotent: false, + }), inspect_macho: nativeEvidence, inspect_signature: nativeEvidence, inspect_plist: nativeEvidence, @@ -194,6 +223,7 @@ export const TOOL_EFFECTS: Readonly> = { decode_interface_builder: evidence, inspect_asset_catalog: nativeEvidence, inspect_keyed_archive: evidence, + trace_dylib_resolution: evidence, inspect_managed_artifact: evidence, inspect_managed_members: evidence, inspect_managed_native_boundaries: evidence, diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index d939ff2379..3fd1575ed9 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -4,6 +4,7 @@ import { nativeLoadImageSchema } from "../domain/native/nativeLoadImage.js"; import { nativeUiResultSchema } from "../domain/native/nativeUiObservation.js"; import { nativeValueTraceSchema } from "../domain/native/nativeValueTrace.js"; import { nativeDataTypeSchema } from "../domain/native/nativeDataType.js"; +import { nativeCallObservationResultSchema } from "../domain/native/nativeCallObservation.js"; import { nativeInstructionSchema, nativeCallTargetsSchema, @@ -15,7 +16,7 @@ import { evidenceBundleSchema } from "../domain/evidenceBundle.js"; import { processCaptureComparisonSchema, processCaptureSchema, -} from "../domain/processCapture.js"; +} from "../domain/process/processCapture.js"; import { functionInstructionWindowSchema, referenceKindSchema, @@ -31,6 +32,7 @@ import { import { artifactExtractionResultSchema } from "../domain/artifactGraph.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js"; +import { dylibResolutionResultSchema } from "../domain/apple/dylibResolution.js"; import { keyedArchiveResultSchema } from "../domain/apple/keyedArchive.js"; import { appleAssetCatalogResultSchema } from "../domain/apple/appleAssetCatalog.js"; import { @@ -322,6 +324,7 @@ export const enhancedOutputSchemas: Readonly> = { export const nativeOutputSchemas: Readonly> = { observe_native_ui: resultOf(nativeUiResultSchema), capture_native_ui_scenario: resultOf(nativeUiResultSchema), + observe_native_calls: resultOf(nativeCallObservationResultSchema), inspect_macho: resultOf(inspectMachoSchema), inspect_signature: resultOf(inspectSignatureSchema), inspect_plist: resultOf(inspectPlistSchema), @@ -336,6 +339,7 @@ export const artifactOutputSchemas: Readonly> = { decode_interface_builder: resultOf(interfaceBuilderAnalysisSchema), inspect_keyed_archive: resultOf(keyedArchiveResultSchema), inspect_asset_catalog: resultOf(appleAssetCatalogResultSchema), + trace_dylib_resolution: resultOf(dylibResolutionResultSchema), }; /** Exact Evidence schema for execution-free managed static analysis. */ diff --git a/src/contracts/toolOutputSchemaPrimitives.ts b/src/contracts/toolOutputSchemaPrimitives.ts index bf4a4e25e9..9e064c2c3b 100644 --- a/src/contracts/toolOutputSchemaPrimitives.ts +++ b/src/contracts/toolOutputSchemaPrimitives.ts @@ -363,7 +363,7 @@ export const sessionProvider = z negotiated_protocol_version: z.string().nullable(), client: z.object({ name: z.string(), version: z.string() }).nullable(), skill: z.object({ name: z.string(), expected_version: z.string() }), - catalog: z.record(z.string(), z.json()), + catalog: z.record(z.string(), jsonValueSchema), protocol_features: z.object({ progress: z.boolean(), cancellation: z.boolean(), diff --git a/src/contracts/toolSchemaMetadata.test.ts b/src/contracts/toolSchemaMetadata.test.ts new file mode 100644 index 0000000000..1753bd9498 --- /dev/null +++ b/src/contracts/toolSchemaMetadata.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; + +import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js"; +import { + toolInputSchemaWithMetadata, + toolOutputSchemaWithMetadata, +} from "./toolSchemaMetadata.js"; + +const target = "draft-2020-12"; + +describe("advertised tool JSON Schema", () => { + it("projects each canonical contract once per target", () => { + for (const contract of TOOL_CONTRACTS) { + const input = toolInputSchemaWithMetadata(contract)["~standard"]; + const output = toolOutputSchemaWithMetadata(contract)["~standard"]; + const advertisedInput = input.jsonSchema.input({ target }); + const advertisedOutput = output.jsonSchema.output({ target }); + + expect(input.jsonSchema.input({ target }), contract.name).toBe( + advertisedInput, + ); + expect(output.jsonSchema.output({ target }), contract.name).toBe( + advertisedOutput, + ); + expect(advertisedInput.examples, contract.name).toEqual( + contract.examples.map(({ input: example }) => example), + ); + expect(advertisedOutput, contract.name).toEqual( + contract.outputSchema["~standard"].jsonSchema.output({ target }), + ); + } + }); + + it("keeps other targets and library options distinct", () => { + const contract = toolContract("open_binary"); + const input = toolInputSchemaWithMetadata(contract)["~standard"]; + const latest = input.jsonSchema.input({ target }); + const draft07 = input.jsonSchema.input({ target: "draft-07" }); + const libraryOptions = { unrepresentable: "any" }; + const configured = input.jsonSchema.input({ target, libraryOptions }); + + expect(latest.$schema).toBe("https://json-schema.org/draft/2020-12/schema"); + expect(draft07.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(input.jsonSchema.input({ target: "draft-07" })).toBe(draft07); + expect(configured).not.toBe(latest); + expect(input.jsonSchema.input({ target, libraryOptions })).not.toBe( + configured, + ); + expect( + toolInputSchemaWithMetadata(contract).safeParse( + contract.examples[0]?.input, + ).success, + ).toBe(true); + }); +}); diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts index 79c477b613..bed9526ba0 100644 --- a/src/contracts/toolSchemaMetadata.ts +++ b/src/contracts/toolSchemaMetadata.ts @@ -47,37 +47,124 @@ const PROPERTY_DESCRIPTIONS: Readonly> = { unknown_id: "Exact residual-unknown identifier.", }; +type JsonSchemaProjection = z.ZodType["~standard"]["jsonSchema"]["input"]; + /** Attach caller guidance to a canonical schema for the SDK wire projection. */ export const toolInputSchemaWithMetadata = ( contract: Contract, -): Contract["inputSchema"] => { - const schema = contract.inputSchema.meta( - z.globalRegistry.get(contract.inputSchema) ?? {}, +): Contract["inputSchema"] => + withAdvertisedJsonSchema( + contract.inputSchema, + "input", + (project) => (options) => + flattenRootUnion({ + ...describeProperties(project(options)), + examples: contract.examples.map(({ input }) => input), + }), ); - const standard = schema["~standard"]; - const inputJsonSchema = standard.jsonSchema?.input; - if (inputJsonSchema === undefined) - throw new TypeError( - "Tool input schema does not expose Standard JSON Schema", - ); +/** Advertise a canonical output schema without reconverting it per listing. */ +export const toolOutputSchemaWithMetadata = ( + contract: Contract, +): Contract["outputSchema"] => + withAdvertisedJsonSchema(contract.outputSchema, "output"); + +const withAdvertisedJsonSchema = ( + canonical: Schema, + io: "input" | "output", + advertise: (project: JsonSchemaProjection) => JsonSchemaProjection = ( + project, + ) => project, +): Schema => { // Zod's input projection drops root metadata when a descendant transforms. // Preserve the parser and let the SDK own conversion of everything else. + const schema = canonical.meta(z.globalRegistry.get(canonical) ?? {}); + const standard = schema["~standard"]; + const project = standard.jsonSchema?.[io]; + if (project === undefined) + throw new TypeError( + `Tool ${io} schema does not expose Standard JSON Schema`, + ); Object.defineProperty(schema, "~standard", { value: { ...standard, jsonSchema: { ...standard.jsonSchema, - input: (options: Parameters[0]) => ({ - ...describeProperties(inputJsonSchema(options)), - examples: contract.examples.map(({ input }) => input), - }), + [io]: memoizeByTarget(advertise(project)), }, }, }); return schema; }; +// The SDK reconverts every registered schema on each `tools/list`, although a +// registered contract cannot change. Like the SDK's own memoized output +// projection, the advertised value is shared and treated as read-only. +const memoizeByTarget = ( + project: JsonSchemaProjection, +): JsonSchemaProjection => { + const byTarget = new Map>(); + return (options) => { + if (options.libraryOptions !== undefined) return project(options); + const cached = byTarget.get(options.target); + if (cached !== undefined) return cached; + const projected = project(options); + byTarget.set(options.target, projected); + return projected; + }; +}; + +/** Keep MCP tool roots object-shaped for clients that reject root unions. */ +const flattenRootUnion = ( + value: Record, +): Record => { + if (!Array.isArray(value.anyOf)) return value; + const branches = value.anyOf.filter(isObject); + if (branches.length === 0 || branches.length !== value.anyOf.length) + return value; + + const properties: Record = {}; + for (const branch of branches) { + if (!isObject(branch.properties)) return value; + for (const [name, schema] of Object.entries(branch.properties)) { + const previous = properties[name]; + if (previous === undefined) properties[name] = schema; + else if (JSON.stringify(previous) !== JSON.stringify(schema)) + properties[name] = { + anyOf: [previous, schema], + description: fallbackPropertyDescription(name), + }; + } + } + + // The canonical Zod parser still enforces branch validation at invocation. + const { anyOf: _anyOf, required: _required, ...root } = value; + const requiredByBranch = branches.map((branch) => + Array.isArray(branch.required) + ? branch.required.filter( + (item): item is string => typeof item === "string", + ) + : [], + ); + const required = requiredByBranch.reduce((common, current) => + common.filter((name) => current.includes(name)), + ); + const minProperties = Math.max( + typeof root.minProperties === "number" ? root.minProperties : 0, + Math.min(...requiredByBranch.map((names) => names.length)), + ); + return { + ...root, + type: "object", + properties, + ...(minProperties > 0 ? { minProperties } : {}), + ...(required.length > 0 ? { required } : {}), + ...(branches.every((branch) => branch.additionalProperties === false) + ? { additionalProperties: false } + : {}), + }; +}; + const describeProperties = ( value: Readonly>, ): Record => diff --git a/src/contracts/webNetworkCaptureToolContracts.ts b/src/contracts/webNetworkCaptureToolContracts.ts new file mode 100644 index 0000000000..6acfa2241e --- /dev/null +++ b/src/contracts/webNetworkCaptureToolContracts.ts @@ -0,0 +1,30 @@ +import { + inspectWebNetworkCaptureInputSchema, + webNetworkCaptureSchema, +} from "../domain/webNetworkCapture.js"; +import type { ToolContract } from "./toolContracts.js"; +import { toolContractMetadata } from "./toolEffects.js"; +import { evidenceResultOf } from "./toolOutputSchemas.js"; + +/** Historical inspection is distinct from live browser capture and runtime authority. */ +export const WEB_NETWORK_CAPTURE_TOOL_CONTRACTS = [ + { + name: "inspect_web_network_capture", + ...toolContractMetadata("inspect_web_network_capture"), + description: + "Inspect retained local HAR 1.2 or native mitmproxy capture records without contacting recorded URLs. Returns original ordinals, pointers/byte ranges, preserved producer fields, precise numeric representations and exact exposed binary bytes inline. Excludes known transport credentials and explicitly marked values. Decodes every record before optional ordinal selection; malformed or oversized captures return no partial success. Duplicate HAR members are invalid; prototype-named members such as __proto__ are preserved as ordinary own JSON members without prototype mutation. Uses pinned upstream parsers, owned temporary files and bounded offline processes. Native mitmproxy requires caller-supplied mitmdump 12.2.3 on Linux. Browser transaction IDs, runtime attribution and original bytes of HAR Unicode text remain unknown.", + kind: "browser-provider", + inputSchema: inspectWebNetworkCaptureInputSchema, + outputSchema: evidenceResultOf(webNetworkCaptureSchema), + examples: [ + { + title: "Inspect historical HAR entries", + input: { + capture_path: "/captures/session.har", + format: "har", + record_ordinals: [0, 2], + }, + }, + ], + }, +] as const satisfies readonly ToolContract[]; diff --git a/src/domain/analysisAccessDenied.test.ts b/src/domain/analysisAccessDenied.test.ts new file mode 100644 index 0000000000..2f5ce5c43e --- /dev/null +++ b/src/domain/analysisAccessDenied.test.ts @@ -0,0 +1,29 @@ +import { expect, it } from "vitest"; +import { AnalysisAccessDeniedError } from "./analysisErrorCore.js"; +import { projectAnalysisError } from "./analysisErrorProjection.js"; +import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js"; + +it.each(["EACCES", "EPERM"] as const)( + "projects %s as host access denial with correction guidance and a valid public schema", + (systemCode) => { + const error = new AnalysisAccessDeniedError( + "inspect_web_network_capture", + "/selected/capture.har", + systemCode, + { cause: new Error("internal detail") }, + ); + const projected = projectAnalysisError(error); + expect(analysisErrorProjectionSchema.parse(projected)).toMatchObject({ + code: "access_denied", + category: "unavailable", + retryable: false, + details: { + path: "/selected/capture.har", + system_code: systemCode, + boundary: "filesystem-read", + }, + }); + expect(projected.remediation.action).toContain("read access"); + expect(JSON.stringify(projected)).not.toContain("internal detail"); + }, +); diff --git a/src/domain/analysisErrorBase.ts b/src/domain/analysisErrorBase.ts index 2c423fc3d5..47fd251edf 100644 --- a/src/domain/analysisErrorBase.ts +++ b/src/domain/analysisErrorBase.ts @@ -2,10 +2,14 @@ const ANALYSIS_ERROR_TAGS = [ "AnalysisProtocolError", "AnalysisInputError", + "AnalysisAccessDeniedError", + "AnalysisArtifactChangedError", "AnalysisOutputError", "AnalysisCapabilityUnavailableError", + "AnalysisUnsupportedTargetError", "AnalysisCancelledError", "AnalysisTimeoutError", + "AnalysisResourceConstraintError", "ProviderSelectionError", "ProviderAdapterError", "BrowserObservationError", @@ -28,11 +32,39 @@ const ANALYSIS_ERROR_TAGS = [ /** Stable tag for an expected analysis failure. */ export type AnalysisErrorTag = (typeof ANALYSIS_ERROR_TAGS)[number]; +/** Retained command output associated with a typed failure; truncation stays explicit. */ +export interface AnalysisCapturedOutput { + readonly stdout: string; + readonly stderr: string; + readonly truncated: boolean; +} + +/** Optional provider-neutral context that must survive typed error projection. */ +export interface AnalysisErrorOptions extends ErrorOptions { + readonly capturedOutput?: AnalysisCapturedOutput; +} + /** Base class for expected analysis, provider, and session failures. */ export abstract class AnalysisError extends Error { + readonly capturedOutput: AnalysisCapturedOutput | undefined; + constructor(message: string, options?: AnalysisErrorOptions) { + super(message, options); + this.capturedOutput = + options?.capturedOutput === undefined + ? undefined + : { ...options.capturedOutput }; + } abstract readonly _tag: AnalysisErrorTag; readonly userMessage: string | undefined = undefined; readonly userCategory: "cancelled" | undefined = undefined; readonly cleanupIncomplete: boolean = false; readonly cleanupResources: readonly string[] = []; + readonly executionFailure: string | undefined = undefined; + readonly partialObservation: PartialProcessCaptureObservation | undefined = + undefined; + readonly cleanupReport: ProcessCaptureCleanupReport | undefined = undefined; } +import type { + PartialProcessCaptureObservation, + ProcessCaptureCleanupReport, +} from "./process/processCapture.js"; diff --git a/src/domain/analysisErrorCore.ts b/src/domain/analysisErrorCore.ts index e709dabc23..03835a2b36 100644 --- a/src/domain/analysisErrorCore.ts +++ b/src/domain/analysisErrorCore.ts @@ -1,5 +1,36 @@ import type { JsonValue } from "./jsonValue.js"; -import { AnalysisError } from "./analysisErrorBase.js"; +import { + AnalysisError, + type AnalysisErrorOptions, +} from "./analysisErrorBase.js"; + +/** A readable supplied artifact lies outside this operation's supported target formats. */ +export class AnalysisUnsupportedTargetError extends AnalysisError { + readonly _tag = "AnalysisUnsupportedTargetError"; + constructor( + readonly operation: string, + readonly path: string, + readonly reason: string, + options?: AnalysisErrorOptions, + ) { + super(`Unsupported target for ${operation} at ${path}: ${reason}`, options); + } +} + +/** Provider reported a resource failure, distinct from malformed input or unsupported coverage. */ +export class AnalysisResourceConstraintError extends AnalysisError { + readonly _tag = "AnalysisResourceConstraintError"; + + constructor( + readonly operation: string, + readonly resource: "memory" | "cpu" | "file-size", + readonly reason: string, + readonly reportedLimits: Readonly> | null, + options?: AnalysisErrorOptions, + ) { + super(`Resource constraint during ${operation}: ${reason}`, options); + } +} /** Provider-neutral invalid analysis input or output at an application boundary. */ export class AnalysisProtocolError extends AnalysisError { @@ -12,7 +43,7 @@ export class AnalysisInputError extends AnalysisError { constructor( readonly operation: string, - options?: ErrorOptions, + options?: AnalysisErrorOptions, readonly issues: readonly AnalysisInputIssue[] = [], ) { super(`Invalid analysis input for ${operation}`, options); @@ -43,7 +74,7 @@ export class AnalysisOutputError extends AnalysisError { constructor( readonly operation: string, readonly reason: string, - options?: ErrorOptions, + options?: AnalysisErrorOptions, ) { super(`Invalid analysis output for ${operation}: ${reason}`, options); } @@ -58,7 +89,7 @@ export class AnalysisCapabilityUnavailableError extends AnalysisError { readonly providerId: string, readonly operation: string, readonly reason: string, - options?: ErrorOptions & { readonly userMessage?: string }, + options?: AnalysisErrorOptions & { readonly userMessage?: string }, ) { super( `Provider ${providerId} cannot execute ${operation}: ${reason}`, @@ -72,8 +103,11 @@ export class AnalysisCapabilityUnavailableError extends AnalysisError { export class AnalysisCancelledError extends AnalysisError { readonly _tag = "AnalysisCancelledError"; - constructor(readonly operation: string) { - super(`Analysis operation was cancelled: ${operation}`); + constructor( + readonly operation: string, + options?: AnalysisErrorOptions, + ) { + super(`Analysis operation was cancelled: ${operation}`, options); } } @@ -84,9 +118,40 @@ export class AnalysisTimeoutError extends AnalysisError { constructor( readonly operation: string, readonly timeoutMs: number, + options?: AnalysisErrorOptions, ) { super( `Analysis operation timed out after ${String(timeoutMs)}ms: ${operation}`, + options, ); } } + +/** Host filesystem permissions denied the selected read; this is not malformed caller input. */ +export class AnalysisAccessDeniedError extends AnalysisError { + readonly _tag = "AnalysisAccessDeniedError"; + constructor( + readonly operation: string, + readonly path: string, + readonly systemCode: "EACCES" | "EPERM", + options?: ErrorOptions, + ) { + super( + `Host filesystem read access denied (${systemCode}) for ${path} during ${operation}`, + options, + ); + } +} + +/** Selected artifact acquisition observed a change, so no stable identity can be reported. */ +export class AnalysisArtifactChangedError extends AnalysisError { + readonly _tag = "AnalysisArtifactChangedError"; + constructor( + readonly operation: string, + readonly path: string, + readonly reason: string, + options?: ErrorOptions, + ) { + super(`Selected artifact changed during acquisition: ${reason}`, options); + } +} diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index 0eaa65a1ac..42f96ed757 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -1,4 +1,10 @@ -import { AnalysisInputError } from "./analysisErrorCore.js"; +import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisInputError, + AnalysisUnsupportedTargetError, + AnalysisResourceConstraintError, +} from "./analysisErrorCore.js"; import { ArtifactOperationError } from "./artifactOperationError.js"; import { BinaryTargetError, @@ -28,6 +34,14 @@ import { type AnalysisErrorProjection } from "./analysisErrorProjection.js"; export const analysisErrorRemediationAction = ( error: AnalysisError, ): string => { + if (error instanceof AnalysisUnsupportedTargetError) + return "Select a target supported by this operation or choose an operation supporting the reported target format."; + if (error instanceof AnalysisResourceConstraintError) + return error.resource === "cpu" + ? "Review the reported worker CPU limits and observed signal. Retry with sufficient CPU time or a smaller artifact; REA retains tighter inherited limits." + : error.resource === "file-size" + ? "Review the reported worker file-size limits and write failure. Retry with a sufficient file-size allowance for the evidence reply; REA retains tighter inherited limits." + : "Review the reported worker memory limits and available host memory. Retry with sufficient memory or a smaller artifact; REA retains tighter inherited limits."; if (error instanceof HopperTimeoutError) return error.providerState === "busy" ? "Check binary_session.analysis_activity, wait for the active Hopper request to finish, then retry." @@ -47,6 +61,10 @@ export const analysisErrorRemediationAction = ( error.constraint === "directory_requires_file" ) return "For a JavaScript/Electron application directory, call analyze_javascript_application with input_path or run `rea analyze `. For binary analysis, select its executable file."; + if (error instanceof AnalysisAccessDeniedError) + return "Check the current process's read access to the selected path. Retry with a readable local file."; + if (error instanceof AnalysisArtifactChangedError) + return "Wait until the selected file is stable, then retry this operation."; if (error instanceof AnalysisInputError) return "Correct the listed arguments and retry."; if (error instanceof UnknownRegistryError && error.reason === "not-found") @@ -107,18 +125,28 @@ const STATIC_ERROR_CATEGORIES: Readonly< Partial> > = { AnalysisInputError: "invalid_input", + AnalysisAccessDeniedError: "unavailable", + AnalysisArtifactChangedError: "integrity_mismatch", AnalysisCapabilityUnavailableError: "unsupported_provider", + AnalysisUnsupportedTargetError: "unsupported_target", ProviderSelectionError: "unsupported_provider", EvidenceIntegrityError: "integrity_mismatch", AnalysisCancelledError: "cancelled", HopperCancelledError: "cancelled", AnalysisTimeoutError: "timeout", + AnalysisResourceConstraintError: "resource_constraint", HopperTimeoutError: "timeout", NoBinaryOpenError: "unavailable", BinaryTargetError: "unavailable", }; export const analysisErrorUserMessage = (error: AnalysisError): string => { + if (error instanceof AnalysisUnsupportedTargetError) return error.message; + if (error instanceof AnalysisResourceConstraintError) return error.reason; + if (error instanceof AnalysisAccessDeniedError) + return "Host filesystem permissions denied read access to the selected path."; + if (error instanceof AnalysisArtifactChangedError) + return "The selected artifact changed during acquisition; no stable snapshot was decoded."; if (error instanceof AnalysisInputError) return "Analysis input is invalid. Check the arguments and try again."; const hopperMessage = hopperErrorUserMessage(error); @@ -138,8 +166,7 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => { : `Evidence ${error.evidenceId} does not match the requested reference (${error.reason}). Check the expected and actual identity in the diagnostic details.`; if (error instanceof EvidenceIntegrityError) return "Evidence is invalid or has changed. Recreate or re-import it, then try again."; - if (error instanceof EvidenceFileError) - return evidenceFileMessage(error.reason); + if (error instanceof EvidenceFileError) return evidenceFileMessage(error); if (error instanceof UnknownRegistryError && error.reason === "not-found") return "The requested residual unknown does not exist in this session. Check the unknown_id and try again."; if (error instanceof UnknownRegistryError) @@ -243,7 +270,14 @@ const artifactMessage = (reason: ArtifactOperationError["reason"]): string => { return "Artifact could not be read or written. Check file access and try again."; }; -const evidenceFileMessage = (reason: EvidenceFileError["reason"]): string => { +const evidenceFileMessage = ({ + operation, + reason, +}: EvidenceFileError): string => { + if (reason === "missing") + return operation === "read" + ? "Evidence file does not exist at the selected path. Check the path and try again." + : "Evidence output directory does not exist. Choose an existing directory and try again."; if (reason === "not-file") return "Evidence path does not point to a regular file. Choose a file and try again."; if (reason === "exists") @@ -256,10 +290,14 @@ const evidenceFileMessage = (reason: EvidenceFileError["reason"]): string => { const KNOWN_ERROR_TAGS = { AnalysisProtocolError: true, AnalysisInputError: true, + AnalysisAccessDeniedError: true, + AnalysisArtifactChangedError: true, AnalysisOutputError: true, AnalysisCapabilityUnavailableError: true, + AnalysisUnsupportedTargetError: true, AnalysisCancelledError: true, AnalysisTimeoutError: true, + AnalysisResourceConstraintError: true, ProviderSelectionError: true, ProviderAdapterError: true, BrowserObservationError: true, diff --git a/src/domain/analysisErrorProjection.test.ts b/src/domain/analysisErrorProjection.test.ts index 6b83265a26..392b97966c 100644 --- a/src/domain/analysisErrorProjection.test.ts +++ b/src/domain/analysisErrorProjection.test.ts @@ -1,8 +1,12 @@ import { describe, expect, it } from "vitest"; import { AnalysisCapabilityUnavailableError, + AnalysisUnsupportedTargetError, AnalysisInputError, AnalysisOutputError, + AnalysisCancelledError, + AnalysisTimeoutError, + AnalysisResourceConstraintError, } from "./analysisErrorCore.js"; import { ArtifactOperationError } from "./artifactOperationError.js"; import { BinaryTargetError } from "./configurationErrors.js"; @@ -16,6 +20,113 @@ import { import { ProviderAdapterError } from "./providerAdapterError.js"; import { UnknownRegistryError } from "./unknownRegistryError.js"; import { projectAnalysisError } from "./analysisErrorProjection.js"; +import { ProviderSelectionError } from "./providerSelectionError.js"; +import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js"; + +const retainedOutput = { + stdout: "selected output\u0000", + stderr: "upstream warning", + truncated: true, +}; + +it("distinguishes unsupported target formats from unavailable provider capabilities", () => { + const projected = projectAnalysisError( + new AnalysisUnsupportedTargetError( + "inspect_evm_interface", + "/selected/runtime.hex", + "EOF-style EF00 container is unsupported.", + { capturedOutput: retainedOutput }, + ), + ); + expect(projected).toMatchObject({ + code: "unsupported_target", + category: "unsupported_target", + retryable: false, + details: { + operation: "inspect_evm_interface", + path: "/selected/runtime.hex", + reason: "EOF-style EF00 container is unsupported.", + captured_output: retainedOutput, + }, + }); + expect(projected.message).toContain("EF00"); + expect(projected.remediation.action).toContain("target format"); + expect(projected.remediation.action).not.toContain("doctor"); + expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(true); +}); + +it.each([ + ["memory", null], + ["memory", { address_space_bytes: 67108864 }], + ["cpu", null], + ["cpu", { cpu_seconds: 1 }], + ["file-size", null], + ["file-size", { file_size_bytes: 1024 }], +] as const)( + "projects reported %s constraints with observed or unknown limits: %j", + (resource, limits) => { + const projected = projectAnalysisError( + new AnalysisResourceConstraintError( + "inspect_binary_layout", + resource, + resource === "memory" + ? "Memory allocation failed; exact cause unknown." + : "Observed SIGXCPU; exact signal cause unknown.", + limits, + { capturedOutput: retainedOutput }, + ), + ); + expect(projected).toMatchObject({ + code: "resource_constraint", + category: "resource_constraint", + retryable: false, + details: { + resource, + reported_limits: limits, + captured_output: retainedOutput, + }, + }); + expect(projected.remediation.action).toContain( + resource === "cpu" + ? "CPU" + : resource === "file-size" + ? "file-size" + : "memory", + ); + expect(projected.remediation.action).not.toContain("doctor"); + expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe( + true, + ); + }, +); +it.each([ + new AnalysisInputError("inspect", { capturedOutput: retainedOutput }), + new AnalysisOutputError("inspect", "invalid reply", { + capturedOutput: retainedOutput, + }), + new AnalysisCapabilityUnavailableError( + "provider", + "inspect", + "unsupported profile", + { capturedOutput: retainedOutput }, + ), + new AnalysisCancelledError("inspect", { capturedOutput: retainedOutput }), + new AnalysisTimeoutError("inspect", 30, { capturedOutput: retainedOutput }), + new ProviderSelectionError({ + operation: "inspect", + reason: "provider_unavailable", + requestedProviderId: "provider", + candidateIds: [], + capturedOutput: retainedOutput, + }), +])( + "retains captured output without changing the typed primary failure: $._tag", + (failure) => { + expect(projectAnalysisError(failure).details?.captured_output).toEqual( + retainedOutput, + ); + }, +); describe("analysis error projection: provider failures", () => { it("projects the primary browser failure alongside incomplete cleanup", () => { diff --git a/src/domain/analysisErrorProjection.ts b/src/domain/analysisErrorProjection.ts index f4e8b6197a..bd8e7e9c9b 100644 --- a/src/domain/analysisErrorProjection.ts +++ b/src/domain/analysisErrorProjection.ts @@ -1,4 +1,4 @@ -import type { JsonValue } from "./jsonValue.js"; +import { jsonValueSchema, type JsonValue } from "./jsonValue.js"; import { analysisErrorCategory, @@ -7,11 +7,15 @@ import { assertKnownAnalysisErrorTag, } from "./analysisErrorPresentation.js"; import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, AnalysisCancelledError, AnalysisCapabilityUnavailableError, + AnalysisUnsupportedTargetError, AnalysisInputError, AnalysisOutputError, AnalysisTimeoutError, + AnalysisResourceConstraintError, } from "./analysisErrorCore.js"; import { ArtifactOperationError } from "./artifactOperationError.js"; import { BinaryTargetError } from "./configurationErrors.js"; @@ -37,7 +41,11 @@ export const projectAnalysisError = ( ): AnalysisErrorProjection => { assertKnownAnalysisErrorTag(error._tag); const code = errorCode(error); - const details = errorDetails(error); + const primaryDetails = errorDetails(error); + const details = + error.capturedOutput === undefined + ? primaryDetails + : { ...primaryDetails, captured_output: { ...error.capturedOutput } }; return { code, category: analysisErrorCategory(error), @@ -130,10 +138,14 @@ const STATIC_ERROR_CODES = { AnalysisOutputError: "unreadable_output", HopperProtocolError: "unreadable_output", AnalysisInputError: "invalid_request", + AnalysisAccessDeniedError: "access_denied", + AnalysisArtifactChangedError: "artifact_changed", AnalysisCapabilityUnavailableError: "capability_unavailable", + AnalysisUnsupportedTargetError: "unsupported_target", AnalysisCancelledError: "cancelled", HopperCancelledError: "cancelled", AnalysisTimeoutError: "provider_timeout", + AnalysisResourceConstraintError: "resource_constraint", HopperTimeoutError: "provider_timeout", HopperProcessError: "provider_unavailable", HopperStartError: "provider_unavailable", @@ -177,6 +189,33 @@ const errorDetails = ( const requestErrorDetails = ( error: AnalysisError, ): Readonly> | undefined => { + if (error instanceof AnalysisUnsupportedTargetError) + return { + operation: error.operation, + path: error.path, + reason: error.reason, + }; + if (error instanceof AnalysisResourceConstraintError) + return { + operation: error.operation, + resource: error.resource, + reason: error.reason, + reported_limits: error.reportedLimits, + }; + if (error instanceof AnalysisArtifactChangedError) + return { + operation: error.operation, + path: error.path, + reason: error.reason, + boundary: "stable-artifact-read", + }; + if (error instanceof AnalysisAccessDeniedError) + return { + operation: error.operation, + path: error.path, + system_code: error.systemCode, + boundary: "filesystem-read", + }; if (error instanceof AnalysisOutputError) return { operation: error.operation, reason: error.reason }; if (error instanceof AnalysisInputError && error.issues.length > 0) @@ -220,7 +259,11 @@ const artifactStateErrorDetails = ( }; if (error instanceof UnknownRegistryError) return { reason: error.reason }; if (error instanceof EvidenceFileError) - return { operation: error.operation, reason: error.reason }; + return { + operation: error.operation, + reason: error.reason, + ...(error.path === undefined ? {} : { path: error.path }), + }; return undefined; }; @@ -336,12 +379,30 @@ const lifecycleErrorDetails = ( return { cleanup: "incomplete", resources: [...error.cleanupResources], + ...(error.cleanupReport === undefined + ? {} + : { cleanup_report: jsonValueSchema.parse(error.cleanupReport) }), + ...(error.executionFailure === undefined + ? {} + : { execution_failure: error.executionFailure }), + ...(error.partialObservation === undefined + ? {} + : { + partial_observation: jsonValueSchema.parse( + error.partialObservation, + ), + }), }; if ( error._tag === "ProcessCaptureError" && error.userCategory === "cancelled" ) return { operation: "process_capture", cleanup: "complete" }; + if ( + error._tag === "ProcessCaptureError" && + error.executionFailure !== undefined + ) + return { execution_failure: error.executionFailure }; if (error instanceof BinaryTargetError) return { path: error.path, @@ -354,6 +415,7 @@ const lifecycleErrorDetails = ( }; const RETRYABLE_CODES: ReadonlySet = new Set([ + "artifact_changed", "invalid_request", "provider_timeout", "cancelled", @@ -366,10 +428,14 @@ export interface AnalysisErrorProjection extends Readonly< > { readonly code: | "invalid_request" + | "access_denied" + | "artifact_changed" | "unreadable_output" | "capability_unavailable" + | "unsupported_target" | "provider_unavailable" | "provider_timeout" + | "resource_constraint" | "cancelled" | "artifact_integrity_mismatch" | "artifact_operation_failed" @@ -384,10 +450,12 @@ export interface AnalysisErrorProjection extends Readonly< readonly category: | "invalid_input" | "unsupported_provider" + | "unsupported_target" | "integrity_mismatch" | "truncated" | "cancelled" | "timeout" + | "resource_constraint" | "unavailable" | "execution_failure"; readonly message: string; diff --git a/src/domain/analysisSnapshot.test.ts b/src/domain/analysisSnapshot.test.ts index da6fd3a612..1ee8cc6208 100644 --- a/src/domain/analysisSnapshot.test.ts +++ b/src/domain/analysisSnapshot.test.ts @@ -17,6 +17,55 @@ import { import { createEvidence } from "./evidence.js"; import { createEvidenceBundle } from "./evidenceBundle.js"; +const snapshotWithHistoricalEvidence = () => { + const target = snapshotTarget(ANALYSIS_SNAPSHOT_TARGET); + const binding = snapshotBinding(ANALYSIS_SNAPSHOT_PROFILE); + const parameters = { procedure: "main" }; + const makeEvidence = (result: string) => + createEvidence(ANALYSIS_SNAPSHOT_TARGET, ANALYSIS_SNAPSHOT_PROVIDER, { + operation: "analyze_function", + parameters, + result: { summary: result }, + analysisProfile: ANALYSIS_SNAPSHOT_PROFILE, + }); + const current = makeEvidence("cached"); + const entry = { + query_id: analysisQueryId(target, binding, "analyze_function", parameters), + operation: "analyze_function", + parameters, + execution: { + result: { summary: "cached" }, + raw_result: null, + provider: binding.provider, + limitations: [], + locations: [], + subject: { + path: ANALYSIS_SNAPSHOT_TARGET.path, + sha256: ANALYSIS_SNAPSHOT_TARGET.sha256, + format: ANALYSIS_SNAPSHOT_TARGET.format, + architecture: ANALYSIS_SNAPSHOT_TARGET.architecture ?? null, + }, + }, + }; + return { + current, + snapshot: { + target, + binding, + entries: [entry], + evidence_bundle: createEvidenceBundle([ + makeEvidence("older"), + current, + createEvidence(ANALYSIS_SNAPSHOT_TARGET, ANALYSIS_SNAPSHOT_PROVIDER, { + operation: "legacy_query", + parameters: {}, + result: { summary: "legacy" }, + }), + ]), + } satisfies AnalysisSnapshot, + }; +}; + describe("analysis snapshot contract", () => { it("preserves DOS MZ target and subject formats with the x86 family", () => { const target = { @@ -91,31 +140,7 @@ describe("analysis snapshot contract", () => { describe("analysis snapshot Evidence binding", () => { it("finds only Evidence committed to the exact binding and profile", () => { - const evidence = createEvidence( - ANALYSIS_SNAPSHOT_TARGET, - ANALYSIS_SNAPSHOT_PROVIDER, - { - operation: "analyze_function", - parameters: { procedure: "main" }, - result: { summary: "cached" }, - analysisProfile: ANALYSIS_SNAPSHOT_PROFILE, - }, - ); - const legacy = createEvidence( - ANALYSIS_SNAPSHOT_TARGET, - ANALYSIS_SNAPSHOT_PROVIDER, - { - operation: "legacy_query", - parameters: {}, - result: { summary: "legacy" }, - }, - ); - const snapshot: AnalysisSnapshot = { - target: snapshotTarget(ANALYSIS_SNAPSHOT_TARGET), - binding: snapshotBinding(ANALYSIS_SNAPSHOT_PROFILE), - entries: [], - evidence_bundle: createEvidenceBundle([evidence, legacy]), - }; + const { current, snapshot } = snapshotWithHistoricalEvidence(); expect( snapshotEvidenceForQuery(snapshot, { target: ANALYSIS_SNAPSHOT_TARGET, @@ -125,7 +150,7 @@ describe("analysis snapshot Evidence binding", () => { provider: ANALYSIS_SNAPSHOT_PROVIDER, evidenceProfile: ANALYSIS_SNAPSHOT_PROFILE, }), - ).toEqual(evidence); + ).toEqual(current); expect( snapshotEvidenceForQuery(snapshot, { target: ANALYSIS_SNAPSHOT_TARGET, diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index 459aa37209..c911448021 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -101,17 +101,33 @@ const entrySchema = z.object({ subject: subjectSchema.nullable(), }), }); +const workflowEntrySchema = z.object({ + query_id: prefixedDigestSchema("query"), + operation: z.string().min(1), + parameters: jsonObjectSchema, + execution: z.object({ + result: jsonValueSchema, + raw_result: jsonValueSchema.nullable(), + provider: committedProviderSchema, + analysis_profile: analysisProfileSchema, + limitations: z.array(z.string()), + locations: z.array(evidenceLocationSchema), + subject: subjectSchema.nullable(), + }), +}); /** Provider- and profile-exact cache of successful immutable analysis calls. */ export const analysisSnapshotSchema = z.object({ target: targetSchema, binding: bindingSchema, entries: z.array(entrySchema), + workflow_entries: z.array(workflowEntrySchema).optional(), evidence_bundle: evidenceBundleSchema, }); export type AnalysisSnapshot = z.infer; export type AnalysisSnapshotEntry = z.infer; +export type AnalysisSnapshotWorkflowEntry = z.infer; export type AnalysisSnapshotTarget = z.infer; export type AnalysisSnapshotBinding = z.infer; @@ -208,6 +224,19 @@ export const snapshotEvidenceForQuery = ( if (!snapshotMatchesBinding(snapshot, target, bindingProfile)) return undefined; const encodedParameters = canonicalJson(parameters); + const queryId = analysisQueryId( + snapshot.target, + snapshot.binding, + operation, + parameters, + ); + const entry = snapshot.entries.find( + (candidate) => candidate.query_id === queryId, + ); + const workflowEntry = snapshot.workflow_entries?.find( + (candidate) => candidate.query_id === queryId, + ); + if (entry === undefined && workflowEntry === undefined) return undefined; return snapshot.evidence_bundle.records.find( (record) => record.subject?.digest.sha256 === target.sha256 && @@ -217,10 +246,77 @@ export const snapshotEvidenceForQuery = ( record.provider.version === provider.version && "analysis_profile" in record && analysisProfilesEqual(record.analysis_profile, evidenceProfile) && - canonicalJson(record.parameters) === encodedParameters, + canonicalJson(record.parameters) === encodedParameters && + ((entry !== undefined && evidenceMatchesEntry(record, entry, snapshot)) || + (workflowEntry !== undefined && + evidenceMatchesWorkflowEntry(record, workflowEntry, snapshot))), ); }; +/** Create one exact replay record for a derived application workflow result. */ +export const createAnalysisSnapshotWorkflowEntry = (input: { + readonly target: AnalysisSnapshotTarget; + readonly binding: AnalysisSnapshotBinding; + readonly operation: string; + readonly parameters: Readonly>; + readonly execution: SnapshotExecution & { + readonly analysisProfile: AnalysisProfileCommitment; + }; +}): AnalysisSnapshotWorkflowEntry => { + const { target, binding, operation, parameters, execution } = input; + if ( + execution.provider.id !== execution.analysisProfile.provider.id || + execution.provider.name !== execution.analysisProfile.provider.name || + execution.provider.version !== execution.analysisProfile.provider.version + ) + throw new TypeError( + "Workflow snapshot execution provider does not match its profile", + ); + return { + query_id: analysisQueryId(target, binding, operation, parameters), + operation, + parameters: jsonObjectSchema.parse(parameters), + execution: { + result: execution.result, + raw_result: execution.rawResult, + provider: execution.analysisProfile.provider, + analysis_profile: execution.analysisProfile, + limitations: [...execution.limitations], + locations: [...execution.locations], + subject: + execution.subject === null + ? null + : { + ...execution.subject, + architecture: execution.subject.architecture ?? null, + }, + }, + }; +}; + +const evidenceMatchesEntry = ( + evidence: Evidence, + entry: AnalysisSnapshotEntry, + snapshot: Pick, +): boolean => + isCorrespondingEvidence(evidence, snapshot) && + canonicalJson({ + query: evidenceQueryKey(evidence), + normalized_result: evidence.normalized_result, + raw_result: evidence.raw_result, + limitations: evidence.limitations, + locations: evidence.locations, + subject: + evidence.subject === null + ? null + : { + local_path: evidence.subject.local_path, + sha256: evidence.subject.digest.sha256, + format: evidence.subject.format, + architecture: evidence.subject.architecture, + }, + }) === entryEvidenceKey(entry, snapshot.binding); + /** Compute the stable lookup key for one provider/profile-specific query. */ export const analysisQueryId = ( target: AnalysisSnapshotTarget, @@ -286,7 +382,9 @@ export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => { ); const ids = new Set(); const boundEntries: AnalysisSnapshotEntry[] = []; + const boundWorkflowEntries: AnalysisSnapshotWorkflowEntry[] = []; const index = createEvidenceIndex(parsed); + const workflowIndex = createWorkflowEvidenceIndex(parsed); for (const entry of parsed.entries) { if ( entry.execution.provider.id !== parsed.binding.provider.id || @@ -316,12 +414,60 @@ export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => { } boundEntries.push(entry); } + for (const entry of parsed.workflow_entries ?? []) { + if ( + entry.execution.provider.id !== + entry.execution.analysis_profile.provider.id || + entry.execution.provider.name !== + entry.execution.analysis_profile.provider.name || + entry.execution.provider.version !== + entry.execution.analysis_profile.provider.version + ) + throw new TypeError( + "Analysis snapshot workflow entry provider does not match its profile", + ); + const expected = analysisQueryId( + parsed.target, + parsed.binding, + entry.operation, + entry.parameters, + ); + if (entry.query_id !== expected) + throw new TypeError( + "Analysis snapshot workflow query identifier does not match", + ); + if (ids.has(entry.query_id)) + throw new TypeError("Analysis snapshot contains duplicate queries"); + ids.add(entry.query_id); + if (!workflowEntryHasMatchingEvidence(entry, workflowIndex)) { + if (workflowEntryHasCorrespondingEvidence(entry, workflowIndex)) + throw new TypeError( + `Analysis snapshot workflow entry ${entry.operation} differs from its Evidence record`, + ); + continue; + } + boundWorkflowEntries.push(entry); + } const sorted = [...parsed.entries].sort((left, right) => left.query_id.localeCompare(right.query_id), ); if (JSON.stringify(parsed.entries) !== JSON.stringify(sorted)) throw new TypeError("Analysis snapshot entries are not canonical"); - return { ...parsed, entries: boundEntries }; + const sortedWorkflows = [...(parsed.workflow_entries ?? [])].sort( + (left, right) => left.query_id.localeCompare(right.query_id), + ); + if ( + parsed.workflow_entries !== undefined && + JSON.stringify(parsed.workflow_entries) !== JSON.stringify(sortedWorkflows) + ) + throw new TypeError("Analysis snapshot workflow entries are not canonical"); + return { + ...parsed, + entries: boundEntries, + ...(parsed.workflow_entries === undefined + ? {} + : { workflow_entries: boundWorkflowEntries }), + }; }; /** Check that a cached provider execution is represented by bundled Evidence. */ @@ -429,6 +575,108 @@ const entryEvidenceKey = ( }, }); +const workflowEntryQueryKey = (entry: AnalysisSnapshotWorkflowEntry): string => + canonicalJson({ + operation: entry.operation, + parameters: entry.parameters, + provider: entry.execution.provider, + analysis_profile: entry.execution.analysis_profile, + }); + +const workflowEntryEvidenceKey = ( + entry: AnalysisSnapshotWorkflowEntry, +): string => + canonicalJson({ + query: workflowEntryQueryKey(entry), + normalized_result: entry.execution.result, + raw_result: entry.execution.raw_result, + limitations: entry.execution.limitations, + locations: entry.execution.locations, + subject: + entry.execution.subject === null + ? null + : { + local_path: entry.execution.subject.path, + sha256: entry.execution.subject.sha256, + format: entry.execution.subject.format, + architecture: entry.execution.subject.architecture, + }, + }); + +const isWorkflowEvidence = ( + evidence: Evidence, + snapshot: Pick, + entry: AnalysisSnapshotWorkflowEntry, +): boolean => + evidence.predicate_type === "rea.analysis" && + evidence.confidence === "derived" && + evidence.authority === "shipped-artifact" && + evidence.subject?.digest.sha256 === snapshot.target.sha256 && + "analysis_profile" in evidence && + analysisProfilesEqual( + evidence.analysis_profile, + entry.execution.analysis_profile, + ) && + evidence.provider.id === entry.execution.provider.id && + evidence.provider.name === entry.execution.provider.name && + evidence.provider.version === entry.execution.provider.version; + +const createWorkflowEvidenceIndex = ( + snapshot: Pick, +): EvidenceIndex => { + const matching = new Set(); + const corresponding = new Set(); + for (const evidence of snapshot.evidence_bundle.records) { + if ( + evidence.predicate_type !== "rea.analysis" || + evidence.confidence !== "derived" || + evidence.authority !== "shipped-artifact" || + evidence.subject?.digest.sha256 !== snapshot.target.sha256 || + !("analysis_profile" in evidence) + ) + continue; + corresponding.add(evidenceQueryKey(evidence)); + matching.add(canonicalJson(evidenceContentProjection(evidence))); + } + return { matching, corresponding }; +}; + +const workflowEntryHasMatchingEvidence = ( + entry: AnalysisSnapshotWorkflowEntry, + index: EvidenceIndex, +): boolean => index.matching.has(workflowEntryEvidenceKey(entry)); + +const workflowEntryHasCorrespondingEvidence = ( + entry: AnalysisSnapshotWorkflowEntry, + index: EvidenceIndex, +): boolean => index.corresponding.has(workflowEntryQueryKey(entry)); + +const evidenceMatchesWorkflowEntry = ( + evidence: Evidence, + entry: AnalysisSnapshotWorkflowEntry, + snapshot: Pick, +): boolean => + isWorkflowEvidence(evidence, snapshot, entry) && + canonicalJson(evidenceContentProjection(evidence)) === + workflowEntryEvidenceKey(entry); + +const evidenceContentProjection = (evidence: Evidence) => ({ + query: evidenceQueryKey(evidence), + normalized_result: evidence.normalized_result, + raw_result: evidence.raw_result, + limitations: evidence.limitations, + locations: evidence.locations, + subject: + evidence.subject === null + ? null + : { + local_path: evidence.subject.local_path, + sha256: evidence.subject.digest.sha256, + format: evidence.subject.format, + architecture: evidence.subject.architecture, + }, +}); + /** Serialize a validated snapshot with byte-stable canonical entry ordering. */ export const serializeAnalysisSnapshot = (snapshot: AnalysisSnapshot): string => `${canonicalJson(parseAnalysisSnapshot(snapshot))}\n`; diff --git a/src/domain/android/androidApplication.ts b/src/domain/android/androidApplication.ts index 0a48a8a984..55718a664d 100644 --- a/src/domain/android/androidApplication.ts +++ b/src/domain/android/androidApplication.ts @@ -118,6 +118,7 @@ export const projectAndroidApplication = ( ? ["Source inventory pages are incomplete; absence is unknown."] : []), "Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.", + "Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid DEX or JVM class bytes.", "Bridge candidates are path-based hypotheses, not decoded JNI declarations or observed runtime calls.", ]; const withoutId = { @@ -176,11 +177,17 @@ const runtimeFamilies = (all: readonly Component[]) => { const families = new Set< AndroidApplicationProjectionResult["runtime_families"][number] >(); - if (all.some(({ path, format }) => format === "dex" || path.endsWith(".dex"))) + if ( + all.some( + ({ path, format }) => + format === "dex" || path.toLowerCase().endsWith(".dex"), + ) + ) families.add("dalvik-art"); if ( all.some( - ({ path, format }) => format === "jvm-class" || path.endsWith(".class"), + ({ path, format }) => + format === "jvm-class" || path.toLowerCase().endsWith(".class"), ) ) families.add("java-kotlin"); diff --git a/src/domain/apple/dyldPaths.ts b/src/domain/apple/dyldPaths.ts new file mode 100644 index 0000000000..765bb15d14 --- /dev/null +++ b/src/domain/apple/dyldPaths.ts @@ -0,0 +1,110 @@ +import type { + DylibTreeEntry, + DylibTreeView, + MachoSlice, +} from "./dylibResolution.js"; + +const MAX_SYMLINK_HOPS = 32; + +export type PathLookup = + | { readonly kind: "file"; readonly path: string } + | { readonly kind: "absent" | "escapes" | "not-file" }; + +/** + * Resolve a root-relative path one segment at a time, as `open` would: + * symlinks are followed inside the root, `..` applies to the real parent, + * and any escape above the root or through an absolute link stops. + */ +export const resolveTreePath = async ( + view: DylibTreeView, + input: string, +): Promise => { + const pending = segmentsOf(input); + const resolved: string[] = []; + let last: DylibTreeEntry | undefined; + let hops = 0; + while (pending.length > 0) { + const segment = pending.shift() ?? ""; + if (segment === "..") { + if (resolved.length === 0) return { kind: "escapes" }; + resolved.pop(); + last = { kind: "directory" }; + continue; + } + const path = [...resolved, segment].join("/"); + const entry = await view.entry(path); + if (entry === undefined) return { kind: "absent" }; + if (entry.kind === "symlink") { + hops += 1; + if (hops > MAX_SYMLINK_HOPS) return { kind: "absent" }; + if (entry.target.startsWith("/")) return { kind: "escapes" }; + pending.unshift(...segmentsOf(entry.target)); + continue; + } + if (entry.kind === "file" && pending.length > 0) return { kind: "absent" }; + resolved.push(segment); + last = entry; + } + return last?.kind === "file" + ? { kind: "file", path: resolved.join("/") } + : { kind: "not-file" }; +}; + +const segmentsOf = (path: string): string[] => + path.split("/").filter((segment) => segment !== "" && segment !== "."); + +export const directoryOf = (path: string): string => { + const index = path.lastIndexOf("/"); + return index < 0 ? "" : path.slice(0, index); +}; + +export const joinPath = (directory: string, rest: string): string => + directory === "" ? rest : rest === "" ? directory : `${directory}/${rest}`; + +export type Expansion = + | { readonly scope: "target"; readonly path: string } + | { readonly scope: "outside"; readonly path: string } + | { readonly scope: "undetermined"; readonly path: string }; + +/** Expand one dyld path prefix relative to its owning image. */ +export const expandPrefix = ( + raw: string, + context: { + readonly ownerDirectory: string; + readonly executable: string | null; + }, +): Expansion => { + const variable = /^@(executable_path|loader_path)(?:\/(.*))?$/u.exec(raw); + if (variable !== null) { + const rest = variable[2] ?? ""; + if (variable[1] === "loader_path") + return { scope: "target", path: joinPath(context.ownerDirectory, rest) }; + return context.executable === null + ? { scope: "undetermined", path: raw } + : { + scope: "target", + path: joinPath(directoryOf(context.executable), rest), + }; + } + if (raw.startsWith("/")) return { scope: "outside", path: raw }; + return { scope: "undetermined", path: raw }; +}; + +/** + * Slices a process may load, best first, following dyld's graded + * architectures: an x86_64h process also accepts generic x86_64. + */ +const COMPATIBLE_ARCHITECTURES: Readonly> = { + x86_64h: ["x86_64h", "x86_64"], +}; + +export const compatibleSlice = ( + slices: readonly MachoSlice[], + process: string, +): MachoSlice | undefined => { + for (const architecture of COMPATIBLE_ARCHITECTURES[process] ?? [process]) { + const slice = slices.find((item) => item.architecture === architecture); + if (slice !== undefined) return slice; + } + return undefined; +}; diff --git a/src/domain/apple/dylibResolution.test.ts b/src/domain/apple/dylibResolution.test.ts new file mode 100644 index 0000000000..bfd0a25b58 --- /dev/null +++ b/src/domain/apple/dylibResolution.test.ts @@ -0,0 +1,605 @@ +import { describe, expect, it } from "vitest"; + +import { + traceDylibLoading, + type DylibTreeEntry, + type DylibTreeView, + type MachoDependency, + type MachoImageFacts, + type MachoSlice, +} from "./dylibResolution.js"; + +const slice = (overrides: Partial = {}): MachoSlice => ({ + architecture: "arm64", + file_type: "dylib", + install_name: null, + dependencies: [], + rpaths: [], + dyld_environment: [], + code_signature_present: true, + ...overrides, +}); + +const dependency = ( + installName: string, + overrides: Partial = {}, +): MachoDependency => ({ + command: "LC_LOAD_DYLIB", + encoding: "dylib_command", + install_name: installName, + weak: false, + upward: false, + reexport: false, + delayed_init: false, + current_version: "1.0.0", + compatibility_version: "1.0.0", + ...overrides, +}); + +const parsed = (...slices: MachoSlice[]): MachoImageFacts => ({ + status: "parsed", + slices, +}); + +const executable = (overrides: Partial = {}): MachoImageFacts => + parsed(slice({ file_type: "execute", ...overrides })); + +/** In-memory tree: files are images (or "data"), symlinks map to targets. */ +const memoryView = ( + files: Readonly>, + symlinks: Readonly> = {}, +): DylibTreeView => { + const entries = new Map(); + const addParents = (path: string): void => { + const segments = path.split("/"); + for (let length = 1; length < segments.length; length++) + entries.set(segments.slice(0, length).join("/"), { kind: "directory" }); + }; + for (const path of Object.keys(files)) { + addParents(path); + entries.set(path, { kind: "file" }); + } + for (const [path, target] of Object.entries(symlinks)) { + addParents(path); + entries.set(path, { kind: "symlink", target }); + } + return { + entry: (path) => Promise.resolve(entries.get(path)), + image: (path) => { + const facts = files[path]; + return Promise.resolve( + facts === undefined || facts === "data" + ? { status: "not-mach-o" as const } + : facts, + ); + }, + }; +}; + +const MAIN = "Contents/MacOS/App"; + +const edgeFor = ( + trace: Awaited>, + loader: string, + installName: string, +) => { + const edge = trace.edges.find( + (candidate) => + candidate.loader === loader && candidate.install_name === installName, + ); + if (edge === undefined) + throw new Error(`missing ${loader} -> ${installName}`); + return edge; +}; + +describe("dyld path expansion", () => { + it("searches the loading image's rpaths before its loaders'", async () => { + const core = "Contents/Frameworks/Core.framework/Versions/A/Core"; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: ["@executable_path/../Frameworks"], + dependencies: [dependency("@rpath/Core.framework/Versions/A/Core")], + }), + [core]: parsed( + slice({ + rpaths: ["@loader_path/Libraries"], + dependencies: [dependency("@rpath/libchain.dylib")], + }), + ), + "Contents/Frameworks/libchain.dylib": parsed(slice()), + "Contents/Frameworks/Core.framework/Versions/A/Libraries/libchain.dylib": + parsed(slice()), + }), + { roots: [MAIN] }, + ); + const chain = edgeFor(trace, core, "@rpath/libchain.dylib"); + expect(chain.via).toEqual([MAIN, core]); + expect(chain.candidates).toEqual([ + expect.objectContaining({ + source: "rpath", + rpath: "@loader_path/Libraries", + rpath_owner: core, + outcome: "resolved", + }), + ]); + expect(chain.resolution).toEqual({ + status: "resolved", + image: + "Contents/Frameworks/Core.framework/Versions/A/Libraries/libchain.dylib", + }); + }); + + it("expands @loader_path in an rpath relative to the image that owns it", async () => { + const plugin = "Contents/PlugIns/Tool.bundle/Contents/MacOS/Tool"; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: ["@loader_path/../Shared"], + dependencies: [ + dependency( + `@executable_path/../PlugIns/Tool.bundle/Contents/MacOS/Tool`, + ), + ], + }), + [plugin]: parsed( + slice({ + file_type: "bundle", + dependencies: [dependency("@rpath/libshared.dylib")], + }), + ), + "Contents/Shared/libshared.dylib": parsed(slice()), + }), + { roots: [MAIN] }, + ); + expect(edgeFor(trace, plugin, "@rpath/libshared.dylib").candidates).toEqual( + [ + expect.objectContaining({ + path: "Contents/MacOS/../Shared/libshared.dylib", + rpath_owner: MAIN, + outcome: "resolved", + resolved_path: "Contents/Shared/libshared.dylib", + }), + ], + ); + }); + + it("gives each executable root its own @executable_path", async () => { + const service = "Contents/XPCServices/Fetch.xpc/Contents/MacOS/Fetch"; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + dependencies: [dependency("@executable_path/libhere.dylib")], + }), + [service]: executable({ + dependencies: [dependency("@executable_path/libhere.dylib")], + }), + "Contents/MacOS/libhere.dylib": parsed(slice()), + }), + { roots: [MAIN, service] }, + ); + expect( + edgeFor(trace, MAIN, "@executable_path/libhere.dylib").resolution.status, + ).toBe("resolved"); + expect( + edgeFor(trace, service, "@executable_path/libhere.dylib"), + ).toMatchObject({ + candidates: [ + { + path: "Contents/XPCServices/Fetch.xpc/Contents/MacOS/libhere.dylib", + outcome: "absent", + }, + ], + resolution: { status: "unresolved", image: null }, + }); + }); + + it("follows Versions/Current inside the root and stops at escaping links", async () => { + const trace = await traceDylibLoading( + memoryView( + { + [MAIN]: executable({ + rpaths: ["@executable_path/../Frameworks"], + dependencies: [ + dependency("@rpath/Core.framework/Core"), + dependency("@rpath/Escape.framework/Escape"), + dependency("@executable_path/../../../outside.dylib"), + ], + }), + "Contents/Frameworks/Core.framework/Versions/A/Core": parsed(slice()), + }, + { + "Contents/Frameworks/Core.framework/Versions/Current": "A", + "Contents/Frameworks/Core.framework/Core": "Versions/Current/Core", + "Contents/Frameworks/Escape.framework": + "/Library/Frameworks/Escape.framework", + }, + ), + { roots: [MAIN] }, + ); + expect( + edgeFor(trace, MAIN, "@rpath/Core.framework/Core").resolution, + ).toEqual({ + status: "resolved", + image: "Contents/Frameworks/Core.framework/Versions/A/Core", + }); + for (const name of [ + "@rpath/Escape.framework/Escape", + "@executable_path/../../../outside.dylib", + ]) + expect(edgeFor(trace, MAIN, name)).toMatchObject({ + candidates: [{ outcome: "escapes-target" }], + resolution: { status: "undetermined", image: null }, + }); + }); +}); + +describe("dyld resolution outcomes", () => { + it("keeps paths outside the root undetermined and in-root fallbacks conditional", async () => { + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: ["/usr/lib/swift", "@executable_path/../Frameworks"], + dependencies: [ + dependency("/usr/lib/libSystem.B.dylib"), + dependency("@rpath/libswiftCore.dylib"), + dependency("libleaf.dylib"), + ], + }), + "Contents/Frameworks/libswiftCore.dylib": parsed(slice()), + }), + { roots: [MAIN] }, + ); + expect(edgeFor(trace, MAIN, "/usr/lib/libSystem.B.dylib")).toMatchObject({ + candidates: [ + { outcome: "outside-target", path: "/usr/lib/libSystem.B.dylib" }, + ], + resolution: { status: "undetermined", image: null }, + }); + expect(edgeFor(trace, MAIN, "@rpath/libswiftCore.dylib")).toMatchObject({ + candidates: [ + { + outcome: "outside-target", + path: "/usr/lib/swift/libswiftCore.dylib", + }, + { outcome: "resolved" }, + ], + resolution: { + status: "conditional", + image: "Contents/Frameworks/libswiftCore.dylib", + }, + }); + expect(edgeFor(trace, MAIN, "libleaf.dylib").resolution.status).toBe( + "undetermined", + ); + }); + + it("derives unresolved and earlier-candidate findings separately for weak loads", async () => { + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: [ + "@executable_path/../Overrides", + "@executable_path/../Frameworks", + ], + dependencies: [ + dependency("@rpath/libfound.dylib"), + dependency("@rpath/libgone.dylib", { + command: "LC_LOAD_WEAK_DYLIB", + weak: true, + }), + dependency("@rpath/libmissing.dylib"), + ], + dyld_environment: ["DYLD_LIBRARY_PATH=/tmp"], + }), + "Contents/Frameworks/libfound.dylib": parsed(slice()), + }), + { roots: [MAIN] }, + ); + expect( + trace.findings.map(({ kind, edge_index: index }) => [kind, index]), + ).toEqual([ + ["earlier-rpath-candidate-absent", 0], + ["weak-load-unresolved", 1], + ["required-load-unresolved", 2], + ["dyld-environment-present", null], + ]); + expect(trace.findings[0]?.explanation).toContain( + "Contents/MacOS/../Overrides/libfound.dylib", + ); + }); + + it("reports non-Mach-O, malformed and wrong-architecture candidates", async () => { + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: [ + "@executable_path/a", + "@executable_path/b", + "@executable_path/c", + ], + dependencies: [dependency("@rpath/lib.dylib")], + }), + "Contents/MacOS/a/lib.dylib": "data", + "Contents/MacOS/b/lib.dylib": { + status: "malformed", + reason: "load command 0 has invalid cmdsize 0", + }, + "Contents/MacOS/c/lib.dylib": parsed(slice({ architecture: "x86_64" })), + }), + { roots: [MAIN] }, + ); + expect( + edgeFor(trace, MAIN, "@rpath/lib.dylib").candidates.map( + ({ outcome }) => outcome, + ), + ).toEqual(["not-mach-o", "malformed", "architecture-missing"]); + expect(trace.coverage).toEqual({ + status: "partial", + unparsed_images: ["Contents/MacOS/b/lib.dylib"], + roots_without_architecture: [], + }); + expect(trace.images.map(({ path }) => path)).toEqual([ + MAIN, + "Contents/MacOS/b/lib.dylib", + "Contents/MacOS/c/lib.dylib", + ]); + }); +}); + +describe("dyld load order", () => { + it("loads each image once per process and reuses matching install names", async () => { + const a = "Contents/Frameworks/libA.dylib"; + const b = "Contents/Frameworks/libB.dylib"; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: ["@executable_path/../Frameworks"], + dependencies: [ + dependency("@rpath/libA.dylib"), + dependency("@rpath/libB.dylib"), + ], + }), + [a]: parsed( + slice({ + install_name: "@rpath/libA.dylib", + dependencies: [ + dependency("@rpath/libB.dylib"), + dependency("@rpath/libA.dylib"), + ], + }), + ), + [b]: parsed( + slice({ + install_name: "@rpath/libB.dylib", + dependencies: [dependency("@rpath/libA.dylib")], + }), + ), + }), + { roots: [MAIN] }, + ); + expect( + trace.edges.map( + ({ loader, install_name: name }) => + `${loader.split("/").at(-1)}>${name}`, + ), + ).toEqual([ + "App>@rpath/libA.dylib", + "App>@rpath/libB.dylib", + "libA.dylib>@rpath/libB.dylib", + "libA.dylib>@rpath/libA.dylib", + "libB.dylib>@rpath/libA.dylib", + ]); + expect(edgeFor(trace, b, "@rpath/libA.dylib").candidates).toEqual([ + expect.objectContaining({ source: "already-loaded", resolved_path: a }), + ]); + expect(edgeFor(trace, a, "@rpath/libA.dylib").install_name_matches).toBe( + true, + ); + }); + + it("leaves @executable_path undetermined for a library root and filters architectures", async () => { + const library = "Contents/Frameworks/libroot.dylib"; + const trace = await traceDylibLoading( + memoryView({ + [library]: parsed( + slice({ dependencies: [dependency("@executable_path/x.dylib")] }), + ), + [MAIN]: executable(), + }), + { roots: [library, MAIN], architecture: "x86_64" }, + ); + expect(trace.roots).toEqual([]); + expect(trace.coverage.roots_without_architecture).toEqual([library, MAIN]); + const arm = await traceDylibLoading( + memoryView({ + [library]: parsed( + slice({ dependencies: [dependency("@executable_path/x.dylib")] }), + ), + }), + { roots: [library] }, + ); + expect(arm.edges[0]?.candidates).toEqual([ + expect.objectContaining({ + source: "executable_path", + outcome: "undetermined", + }), + ]); + }); + + it("stops when cancelled", async () => { + const controller = new AbortController(); + controller.abort(); + await expect( + traceDylibLoading(memoryView({ [MAIN]: executable() }), { + roots: [MAIN], + signal: controller.signal, + }), + ).rejects.toThrow(); + }); +}); + +describe("dyld slice compatibility and coverage", () => { + it("lets an x86_64h process load generic x86_64 but not arm64e load arm64", async () => { + const files = { + [MAIN]: parsed( + slice({ + architecture: "x86_64h", + file_type: "execute", + dependencies: [dependency("@executable_path/libgeneric.dylib")], + }), + slice({ + architecture: "arm64e", + file_type: "execute", + dependencies: [dependency("@executable_path/libgeneric.dylib")], + }), + ), + "Contents/MacOS/libgeneric.dylib": parsed( + slice({ architecture: "x86_64" }), + slice({ architecture: "arm64" }), + ), + }; + const trace = await traceDylibLoading(memoryView(files), { roots: [MAIN] }); + expect( + trace.edges.map(({ architecture, candidates, resolution }) => [ + architecture, + candidates[0]?.outcome, + resolution.status, + ]), + ).toEqual([ + ["x86_64h", "resolved", "resolved"], + ["arm64e", "architecture-missing", "unresolved"], + ]); + }); + + it("keeps unsupported candidates undeterminable rather than malformed", async () => { + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + dependencies: [dependency("@executable_path/libbig.dylib")], + }), + "Contents/MacOS/libbig.dylib": { + status: "unsupported", + reason: "big-endian Mach-O images are not supported", + }, + }), + { roots: [MAIN] }, + ); + expect(trace.edges[0]).toMatchObject({ + candidates: [{ outcome: "unsupported" }], + resolution: { status: "undetermined", image: null }, + }); + expect( + trace.images.find(({ path }) => path.endsWith("libbig.dylib")), + ).toMatchObject({ + parse_status: "unsupported", + }); + }); + + it("reports unclassified Mach-O files as partial coverage", async () => { + const broken = "Contents/Helpers/broken"; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable(), + [broken]: { + status: "malformed", + reason: "load command 0 has invalid cmdsize 0", + }, + }), + { roots: [MAIN], unclassified: [broken] }, + ); + expect(trace.coverage).toEqual({ + status: "partial", + unparsed_images: [broken], + roots_without_architecture: [], + }); + expect(trace.images.find(({ path }) => path === broken)?.reason).toBe( + "load command 0 has invalid cmdsize 0", + ); + }); +}); + +describe("lazily loaded dependencies", () => { + it("resolves lazy loads without traversing them or reporting launch failure", async () => { + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: ["@executable_path/../Frameworks"], + dependencies: [ + dependency("@rpath/liblazy.dylib", { + command: "LC_LAZY_LOAD_DYLIB", + }), + dependency("@rpath/libmissing.dylib", { + command: "LC_LAZY_LOAD_DYLIB", + }), + ], + }), + "Contents/Frameworks/liblazy.dylib": parsed( + slice({ dependencies: [dependency("@rpath/libdeep.dylib")] }), + ), + }), + { roots: [MAIN] }, + ); + expect(trace.edges.map(({ loader }) => loader)).toEqual([MAIN, MAIN]); + expect(trace.edges[0]?.resolution.status).toBe("resolved"); + expect(trace.findings.map(({ kind }) => kind)).toEqual([ + "lazy-load-unresolved", + ]); + }); +}); + +describe("conditional loads", () => { + it("carries a conditional fallback's uncertainty to its dependents and reuses", async () => { + const VENDOR = "Contents/Frameworks/libvendor.dylib"; + const OTHER = "Contents/Frameworks/libother.dylib"; + const trace = await traceDylibLoading( + memoryView({ + [MAIN]: executable({ + rpaths: ["/opt/vendor/lib", "@executable_path/../Frameworks"], + dependencies: [ + dependency("@rpath/libvendor.dylib"), + dependency("@executable_path/../Frameworks/libother.dylib"), + ], + }), + [VENDOR]: parsed( + slice({ + install_name: "@rpath/libvendor.dylib", + dependencies: [ + dependency("@loader_path/libgone.dylib"), + dependency("@loader_path/libweak.dylib", { weak: true }), + dependency("@loader_path/liblazy.dylib", { + command: "LC_LAZY_LOAD_DYLIB", + }), + ], + }), + ), + [OTHER]: parsed( + slice({ dependencies: [dependency("@rpath/libvendor.dylib")] }), + ), + }), + { roots: [MAIN] }, + ); + expect(edgeFor(trace, MAIN, "@rpath/libvendor.dylib")).toMatchObject({ + resolution: { status: "conditional", image: VENDOR }, + loader_conditional: false, + }); + expect(edgeFor(trace, VENDOR, "@loader_path/libgone.dylib")).toMatchObject({ + resolution: { status: "unresolved" }, + loader_conditional: true, + }); + expect(edgeFor(trace, OTHER, "@rpath/libvendor.dylib")).toMatchObject({ + candidates: [{ source: "already-loaded" }], + resolution: { status: "conditional", image: VENDOR }, + loader_conditional: false, + }); + // Required, weak and lazy findings below the fallback are all qualified. + for (const kind of [ + "required-load-unresolved", + "weak-load-unresolved", + "lazy-load-unresolved", + ]) + expect( + trace.findings.find((finding) => finding.kind === kind)?.explanation, + ).toContain(`${VENDOR} loads only conditionally; if it loads,`); + }); +}); diff --git a/src/domain/apple/dylibResolution.ts b/src/domain/apple/dylibResolution.ts new file mode 100644 index 0000000000..241a50be84 --- /dev/null +++ b/src/domain/apple/dylibResolution.ts @@ -0,0 +1,514 @@ +import { z } from "zod"; + +import { + compatibleSlice, + directoryOf, + expandPrefix, + joinPath, + resolveTreePath, + type Expansion, +} from "./dyldPaths.js"; +import { digestSchema } from "../digests.js"; +import { + DYLIB_RESOLUTION_LIMITATIONS, + deriveFindings, +} from "./dylibResolutionFindings.js"; + +/** Normalized path below the analyzed root: no `.`/`..` or empty segments. */ +const ROOT_RELATIVE_PATH = + /^(?!\/)(?!(?:.*\/)?\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\/$)[^\\\0]+$/u; +const rootRelativePathSchema = z.string().min(1).regex(ROOT_RELATIVE_PATH); + +/** Caller selection of process roots and one CPU slice. */ +export const dylibResolutionInputSchema = z.strictObject({ + roots: z.array(rootRelativePathSchema).min(1).optional(), + architecture: z.enum(["arm64", "arm64e", "x86_64"]).optional(), +}); + +const dependencyCommandSchema = z.enum([ + "LC_LOAD_DYLIB", + "LC_LOAD_WEAK_DYLIB", + "LC_REEXPORT_DYLIB", + "LC_LAZY_LOAD_DYLIB", + "LC_LOAD_UPWARD_DYLIB", +]); + +const dependencySchema = z.strictObject({ + command: dependencyCommandSchema, + encoding: z.enum(["dylib_command", "dylib_use_command"]), + install_name: z.string(), + weak: z.boolean(), + upward: z.boolean(), + reexport: z.boolean(), + delayed_init: z.boolean(), + current_version: z.string(), + compatibility_version: z.string(), +}); + +/** Observed dylib-loading facts of one Mach-O slice. */ +export const machoSliceSchema = z.strictObject({ + architecture: z.string().min(1), + file_type: z.enum(["execute", "dylib", "bundle", "other"]), + install_name: z.string().nullable(), + dependencies: z.array(dependencySchema), + rpaths: z.array(z.string()), + dyld_environment: z.array(z.string()), + code_signature_present: z.boolean(), +}); + +export type MachoDependency = z.infer; +export type MachoSlice = z.infer; + +/** Header parse of one file; malformed and unsupported images keep their reason. */ +export type MachoImageFacts = + | { readonly status: "parsed"; readonly slices: readonly MachoSlice[] } + | { + readonly status: "malformed" | "unsupported"; + readonly reason: string; + } + | { readonly status: "not-mach-o" }; + +/** One observed path below the analyzed root. Symlink targets are not followed by the view. */ +export type DylibTreeEntry = + | { readonly kind: "file" | "directory" } + | { readonly kind: "symlink"; readonly target: string }; + +/** Read-only, lazily probed view of the analyzed root. Paths are root-relative. */ +export interface DylibTreeView { + entry(path: string): Promise; + image(path: string): Promise; +} + +const candidateSchema = z.strictObject({ + path: z.string(), + source: z.enum([ + "literal", + "executable_path", + "loader_path", + "rpath", + "already-loaded", + ]), + rpath: z.string().nullable(), + rpath_owner: z.string().nullable(), + outcome: z.enum([ + "resolved", + "absent", + "not-mach-o", + "malformed", + "unsupported", + "architecture-missing", + "outside-target", + "escapes-target", + "undetermined", + ]), + resolved_path: z.string().nullable(), +}); + +const edgeSchema = z.strictObject({ + root: z.string(), + architecture: z.string(), + loader: z.string(), + via: z.array(z.string()), + ...dependencySchema.shape, + candidates: z.array(candidateSchema), + resolution: z.strictObject({ + status: z.enum(["resolved", "conditional", "unresolved", "undetermined"]), + image: z.string().nullable(), + }), + install_name_matches: z.boolean().nullable(), + /** + * The loader was reached through a conditional edge, so it might not load; + * this edge describes what happens only if it does. + */ + loader_conditional: z.boolean(), +}); + +const findingSchema = z.strictObject({ + kind: z.enum([ + "required-load-unresolved", + "weak-load-unresolved", + "lazy-load-unresolved", + "earlier-rpath-candidate-absent", + "dyld-environment-present", + ]), + edge_index: z.number().int().nonnegative().nullable(), + image: z.string().nullable(), + basis: z.literal("derived"), + explanation: z.string().min(1), +}); + +const imageSchema = z.strictObject({ + path: z.string(), + sha256: digestSchema, + parse_status: z.enum(["parsed", "malformed", "unsupported"]), + reason: z.string().nullable(), + slices: z.array(machoSliceSchema.omit({ dependencies: true })), +}); + +/** Static dyld load-path resolution for every selected process root. */ +export const dylibResolutionResultSchema = z.strictObject({ + root_path: z.string(), + target_sha256: digestSchema, + roots: z.array( + z.strictObject({ image: z.string(), architecture: z.string() }), + ), + images: z.array(imageSchema), + edges: z.array(edgeSchema), + findings: z.array(findingSchema), + coverage: z.strictObject({ + status: z.enum(["complete", "partial"]), + unparsed_images: z.array(z.string()), + roots_without_architecture: z.array(z.string()), + }), + limitations: z.array(z.string().min(1)), +}); + +export type DylibResolutionResult = z.infer; +type Candidate = z.infer; +export type DylibEdge = z.infer; +export type DylibFinding = z.infer; +type Edge = DylibEdge; + +/** Result before the adapter adds file digests and the analyzed root. */ +export type DylibTrace = Omit< + DylibResolutionResult, + "root_path" | "target_sha256" | "images" +> & { + readonly images: readonly (Omit< + DylibResolutionResult["images"][number], + "sha256" + > & { readonly sha256?: never })[]; +}; + +interface LoadedImage { + readonly slice: MachoSlice; + /** Images from the process root to this image; the rpath stack, outermost first. */ + readonly chain: readonly string[]; + /** Some edge on the chain was conditional: an unknown earlier candidate may load instead. */ + readonly conditional: boolean; +} + +interface ProcessContext { + readonly view: DylibTreeView; + readonly root: string; + readonly architecture: string; + readonly executable: string | null; + readonly loaded: Map; + readonly byInstallName: Map; + readonly images: Map; +} + +interface CandidateTemplate { + readonly expansion: Expansion; + readonly source: Candidate["source"]; + readonly rpath: string | null; + readonly rpathOwner: string | null; +} + +const candidateTemplates = ( + dependency: MachoDependency, + loader: string, + context: ProcessContext, +): CandidateTemplate[] => { + const name = dependency.install_name; + if (!name.startsWith("@rpath/")) { + const source = name.startsWith("@executable_path") + ? "executable_path" + : name.startsWith("@loader_path") + ? "loader_path" + : "literal"; + return [ + { + expansion: expandPrefix(name, { + ownerDirectory: directoryOf(loader), + executable: context.executable, + }), + source, + rpath: null, + rpathOwner: null, + }, + ]; + } + const rest = name.slice("@rpath/".length); + const owners = [...(context.loaded.get(loader)?.chain ?? [loader])].reverse(); + return owners.flatMap((owner) => + (context.loaded.get(owner)?.slice.rpaths ?? []).map((rpath) => { + const base = expandPrefix(rpath, { + ownerDirectory: directoryOf(owner), + executable: context.executable, + }); + const expansion: Expansion = { + scope: base.scope, + path: + base.scope === "outside" + ? `${base.path.replace(/\/+$/u, "")}/${rest}` + : joinPath(base.path, rest), + }; + return { expansion, source: "rpath" as const, rpath, rpathOwner: owner }; + }), + ); +}; + +const evaluateCandidate = async ( + template: CandidateTemplate, + context: ProcessContext, +): Promise => { + const base = { + path: template.expansion.path, + source: template.source, + rpath: template.rpath, + rpath_owner: template.rpathOwner, + }; + if (template.expansion.scope === "outside") + return { ...base, outcome: "outside-target", resolved_path: null }; + if (template.expansion.scope === "undetermined") + return { ...base, outcome: "undetermined", resolved_path: null }; + const lookup = await resolveTreePath(context.view, template.expansion.path); + if (lookup.kind === "escapes") + return { ...base, outcome: "escapes-target", resolved_path: null }; + if (lookup.kind !== "file") + return { ...base, outcome: "absent", resolved_path: null }; + const facts = await imageFacts(context, lookup.path); + const outcome: Candidate["outcome"] = + facts.status !== "parsed" + ? facts.status + : compatibleSlice(facts.slices, context.architecture) === undefined + ? "architecture-missing" + : "resolved"; + return { ...base, outcome, resolved_path: lookup.path }; +}; + +const imageFacts = async ( + context: ProcessContext, + path: string, +): Promise => { + const known = context.images.get(path); + if (known !== undefined) return known; + const facts = await context.view.image(path); + context.images.set(path, facts); + return facts; +}; + +const resolution = (candidates: readonly Candidate[]): Edge["resolution"] => { + let external = false; + for (const candidate of candidates) { + if (candidate.outcome === "resolved") + return { + status: external ? "conditional" : "resolved", + image: candidate.resolved_path, + }; + // dyld might load these; REA cannot decide them from the analyzed root. + if ( + candidate.outcome === "outside-target" || + candidate.outcome === "escapes-target" || + candidate.outcome === "unsupported" || + candidate.outcome === "undetermined" + ) + external = true; + } + return { status: external ? "undetermined" : "unresolved", image: null }; +}; + +const resolveDependency = async ( + dependency: MachoDependency, + loader: string, + context: ProcessContext, +): Promise => { + const current = context.loaded.get(loader); + const chain = current?.chain ?? [loader]; + const loaded = context.byInstallName.get(dependency.install_name); + const candidates: Candidate[] = []; + if (loaded !== undefined) + candidates.push({ + path: dependency.install_name, + source: "already-loaded", + rpath: null, + rpath_owner: null, + outcome: "resolved", + resolved_path: loaded, + }); + else + for (const template of candidateTemplates(dependency, loader, context)) { + const candidate = await evaluateCandidate(template, context); + candidates.push(candidate); + // dyld stops at the first loadable candidate. + if (candidate.outcome === "resolved") break; + } + const searched = resolution(candidates); + // Reusing an image that itself loads only conditionally is conditional too. + const resolved: Edge["resolution"] = + loaded !== undefined && + context.loaded.get(loaded)?.conditional === true && + searched.status === "resolved" + ? { ...searched, status: "conditional" } + : searched; + const image = + resolved.image === null ? undefined : context.images.get(resolved.image); + const slice = + image?.status === "parsed" + ? compatibleSlice(image.slices, context.architecture) + : undefined; + return { + root: context.root, + architecture: context.architecture, + loader, + via: [...chain], + ...dependency, + candidates, + resolution: resolved, + install_name_matches: + slice === undefined || slice.install_name === null + ? null + : slice.install_name === dependency.install_name, + loader_conditional: current?.conditional ?? false, + }; +}; + +/** + * Follow dyld4's dependents-first order: resolve every dependency of one + * image, then recurse into the images it newly loaded, in load-command order. + */ +const traceProcess = async ( + context: ProcessContext, + rootSlice: MachoSlice, + signal?: AbortSignal, +): Promise => { + const edges: Edge[] = []; + context.loaded.set(context.root, { + slice: rootSlice, + chain: [context.root], + conditional: false, + }); + if (rootSlice.install_name !== null) + context.byInstallName.set(rootSlice.install_name, context.root); + const pending = [context.root]; + while (pending.length > 0) { + signal?.throwIfAborted(); + const loader = pending.pop() ?? ""; + const current = context.loaded.get(loader); + if (current === undefined) continue; + const created: string[] = []; + for (const dependency of current.slice.dependencies) { + const edge = await resolveDependency(dependency, loader, context); + edges.push(edge); + // dyld loads LC_LAZY_LOAD_DYLIB images on first use, not at launch. + if (dependency.command === "LC_LAZY_LOAD_DYLIB") continue; + const image = edge.resolution.image; + if (image === null || context.loaded.has(image)) continue; + const facts = context.images.get(image); + const slice = + facts?.status === "parsed" + ? compatibleSlice(facts.slices, context.architecture) + : undefined; + if (slice === undefined) continue; + context.loaded.set(image, { + slice, + chain: [...current.chain, image], + // dyld may load an unknown earlier candidate instead of this fallback. + conditional: + current.conditional || edge.resolution.status === "conditional", + }); + if ( + slice.install_name !== null && + !context.byInstallName.has(slice.install_name) + ) + context.byInstallName.set(slice.install_name, image); + created.push(image); + } + pending.push(...created.reverse()); + } + return edges; +}; + +/** Trace dyld load paths from each process root through the analyzed root. */ +export const traceDylibLoading = async ( + view: DylibTreeView, + request: { + readonly roots: readonly string[]; + /** Mach-O files whose root role could not be classified because they did not parse. */ + readonly unclassified?: readonly string[]; + readonly architecture?: string; + readonly signal?: AbortSignal; + }, +): Promise => { + const images = new Map(); + for (const path of request.unclassified ?? []) + images.set(path, await view.image(path)); + const roots: DylibTrace["roots"][number][] = []; + const edges: Edge[] = []; + const withoutArchitecture: string[] = []; + for (const root of request.roots) { + const facts = await view.image(root); + images.set(root, facts); + if (facts.status !== "parsed") continue; + const slices = facts.slices.filter( + ({ architecture }) => + request.architecture === undefined || + architecture === request.architecture, + ); + if (slices.length === 0) withoutArchitecture.push(root); + for (const slice of slices) { + roots.push({ image: root, architecture: slice.architecture }); + edges.push( + ...(await traceProcess( + { + view, + root, + architecture: slice.architecture, + executable: slice.file_type === "execute" ? root : null, + loaded: new Map(), + byInstallName: new Map(), + images, + }, + slice, + request.signal, + )), + ); + } + } + const reported = [...images.entries()] + .filter(([, facts]) => facts.status !== "not-mach-o") + .sort(([left], [right]) => compare(left, right)); + const unparsed = reported + .filter(([, facts]) => facts.status !== "parsed") + .map(([path]) => path); + return { + roots, + images: reported.map(([path, facts]) => ({ + path, + parse_status: + facts.status === "not-mach-o" ? "unsupported" : facts.status, + reason: + facts.status === "parsed" || facts.status === "not-mach-o" + ? null + : facts.reason, + slices: + facts.status === "parsed" ? facts.slices.map(withoutDependencies) : [], + })), + edges, + findings: deriveFindings(edges, roots, images), + coverage: { + status: + unparsed.length === 0 && withoutArchitecture.length === 0 + ? "complete" + : "partial", + unparsed_images: unparsed, + roots_without_architecture: withoutArchitecture, + }, + limitations: DYLIB_RESOLUTION_LIMITATIONS, + }; +}; + +const withoutDependencies = ( + slice: MachoSlice, +): Omit => ({ + architecture: slice.architecture, + file_type: slice.file_type, + install_name: slice.install_name, + rpaths: slice.rpaths, + dyld_environment: slice.dyld_environment, + code_signature_present: slice.code_signature_present, +}); + +const compare = (left: string, right: string): number => + left < right ? -1 : left > right ? 1 : 0; diff --git a/src/domain/apple/dylibResolutionFindings.ts b/src/domain/apple/dylibResolutionFindings.ts new file mode 100644 index 0000000000..109ad0f7f0 --- /dev/null +++ b/src/domain/apple/dylibResolutionFindings.ts @@ -0,0 +1,93 @@ +import type { + DylibEdge as Edge, + DylibFinding as Finding, + DylibTrace, + MachoImageFacts, +} from "./dylibResolution.js"; + +export const deriveFindings = ( + edges: readonly Edge[], + roots: DylibTrace["roots"], + images: ReadonlyMap, +): Finding[] => { + const findings: Finding[] = []; + edges.forEach((edge, index) => { + if (edge.resolution.status === "unresolved") + findings.push(unresolvedFinding(edge, index)); + const resolvedAt = edge.candidates.findIndex( + ({ outcome }) => outcome === "resolved", + ); + const earlier = edge.candidates + .slice(0, Math.max(resolvedAt, 0)) + .filter( + ({ source, outcome }) => source === "rpath" && outcome === "absent", + ) + .map(({ path }) => path); + if (resolvedAt > 0 && earlier.length > 0) + findings.push({ + kind: "earlier-rpath-candidate-absent", + edge_index: index, + image: edge.loader, + basis: "derived", + explanation: `dyld searches ${earlier.join(", ")} before ${edge.resolution.image ?? edge.install_name}. A Mach-O placed at an earlier path would load first unless code-signing library validation rejects it; library validation is not evaluated here (see inspect_signature).`, + }); + }); + for (const { image, architecture } of roots) { + const facts = images.get(image); + const environment = + facts?.status === "parsed" + ? (facts.slices.find((slice) => slice.architecture === architecture) + ?.dyld_environment ?? []) + : []; + if (environment.length > 0) + findings.push({ + kind: "dyld-environment-present", + edge_index: null, + image, + basis: "derived", + explanation: `${image} (${architecture}) sets dyld environment variables through LC_DYLD_ENVIRONMENT (${environment.join(", ")}); search paths they add are not modeled.`, + }); + } + return findings; +}; + +const unresolvedFinding = (edge: Edge, index: number): Finding => { + const base = { + edge_index: index, + image: edge.loader, + basis: "derived", + } as const; + const missing = `No candidate for ${edge.command === "LC_LAZY_LOAD_DYLIB" ? "lazily loaded " : edge.weak ? "weak dependency " : ""}${edge.install_name} exists in the analyzed root`; + // Below a conditional fallback, every consequence applies only if it loads. + const when = edge.loader_conditional + ? `. ${edge.loader} loads only conditionally; if it loads, ` + : "; "; + if (edge.command === "LC_LAZY_LOAD_DYLIB") + return { + ...base, + kind: "lazy-load-unresolved", + explanation: `${missing}${when}dyld loads it on first use, which would fail unless the image is supplied elsewhere.`, + }; + return edge.weak + ? { + ...base, + kind: "weak-load-unresolved", + explanation: `${missing}${when}dyld continues without it.`, + } + : { + ...base, + kind: "required-load-unresolved", + explanation: `${missing}${when}dyld would fail to launch ${edge.root} (${edge.architecture}) unless the image is supplied elsewhere.`, + }; +}; + +export const DYLIB_RESOLUTION_LIMITATIONS = [ + "LC_LAZY_LOAD_DYLIB dependencies are resolved but not traversed, because dyld loads them only on first use.", + "Slices are matched by dyld's graded architectures (an x86_64h process also loads x86_64). arm64e processes that disable pointer authentication can also load arm64 slices; that fallback is not modeled.", + "Absolute install names and rpaths are outside the analyzed root and are not evaluated, including /System and /usr/lib libraries that the dyld shared cache usually provides.", + "Leaf and relative install names depend on dyld fallback paths, DYLD_* variables, and the working directory; they are undetermined.", + "Load order follows dyld's dependents-first traversal in load-command order. An image reached through several chains is resolved once per process, with the rpath stack and conditionality of the first chain; a later request whose install name matches an already loaded image reuses it.", + "@loader_path uses each image's symlink-resolved path within the analyzed root.", + "Code-signing checks that can reject a found image, such as library validation and the hardened runtime, are not evaluated.", + "Dylib compatibility versions are not compared. Apple's linker documentation describes a load-time rejection when an image's compatibility version is older than the loader requires, but dyld on macOS 26 was observed to load such an image.", +]; diff --git a/src/domain/apple/interfaceBuilderGraph.ts b/src/domain/apple/interfaceBuilderGraph.ts index fbab951709..37bc094c76 100644 --- a/src/domain/apple/interfaceBuilderGraph.ts +++ b/src/domain/apple/interfaceBuilderGraph.ts @@ -202,53 +202,66 @@ export const buildInterfaceBuilderAnalysis = (input: { }; let hierarchyCount = 0; let hierarchyTruncated = false; - const visitHierarchy = (value: unknown, parentId: string): void => { - if (hierarchyCount >= input.limits.max_objects) { - truncated = true; - hierarchyTruncated = true; - return; - } - if (Array.isArray(value)) { - for (const child of value) visitHierarchy(child, parentId); - return; - } - if (typeof value !== "object" || value === null) return; - const item = record(value); - const objectId = firstString( - item.objectID, - item["object-id"], - item.id, - typeof item.archiveUID === "number" ? String(item.archiveUID) : null, - ); - let nextParent = parentId; - if (objectId !== null) { - const known = objectIds.has(objectId); - const childId = known - ? objectNodeId(objectId) - : `${prefix}unknown:${objectId}`; - if ( - !known && - !addNode({ - id: childId, - kind: "unknown", - name: firstString(item.label, item.name) ?? objectId, - location: null, - attributes: { interface_builder_object_id: objectId }, - evidence: evidenceFor(`hierarchy item ${objectId}`), - }) - ) + const visitHierarchy = ( + initial: unknown, + initialParentId: string, + ): void => { + const pending: { readonly value: unknown; readonly parentId: string }[] = + [{ value: initial, parentId: initialParentId }]; + while (pending.length > 0) { + if (hierarchyCount >= input.limits.max_objects) { + truncated = true; hierarchyTruncated = true; - connect( - `${prefix}hierarchy:${parentId}:${childId}`, - parentId, - childId, - "contains", - `hierarchy ${objectId}`, + return; + } + const entry = pending.pop(); + if (entry === undefined) break; + const { value, parentId } = entry; + if (Array.isArray(value)) { + for (let index = value.length - 1; index >= 0; index -= 1) { + const child = value[index]; + if (child !== undefined) pending.push({ value: child, parentId }); + } + continue; + } + if (typeof value !== "object" || value === null) continue; + const item = record(value); + const objectId = firstString( + item.objectID, + item["object-id"], + item.id, + typeof item.archiveUID === "number" ? String(item.archiveUID) : null, ); - nextParent = childId; - hierarchyCount += 1; + let nextParent = parentId; + if (objectId !== null) { + const known = objectIds.has(objectId); + const childId = known + ? objectNodeId(objectId) + : `${prefix}unknown:${objectId}`; + if ( + !known && + !addNode({ + id: childId, + kind: "unknown", + name: firstString(item.label, item.name) ?? objectId, + location: null, + attributes: { interface_builder_object_id: objectId }, + evidence: evidenceFor(`hierarchy item ${objectId}`), + }) + ) + hierarchyTruncated = true; + connect( + `${prefix}hierarchy:${parentId}:${childId}`, + parentId, + childId, + "contains", + `hierarchy ${objectId}`, + ); + nextParent = childId; + hierarchyCount += 1; + } + pending.push({ value: item.children, parentId: nextParent }); } - visitHierarchy(item.children, nextParent); }; for (const hierarchy of parsed.hierarchy) visitHierarchy(hierarchy, rootId); for (const [index, item] of parsed.connections diff --git a/src/domain/apple/interfaceBuilderKeyedArchive.ts b/src/domain/apple/interfaceBuilderKeyedArchive.ts index d1ee5c3ea1..f354af8ad7 100644 --- a/src/domain/apple/interfaceBuilderKeyedArchive.ts +++ b/src/domain/apple/interfaceBuilderKeyedArchive.ts @@ -196,39 +196,85 @@ const keyedArchiveTable = (objectTable: readonly unknown[]) => { }; const hierarchy = (start: unknown): unknown[] => { const visited = new Set(); - const node = ( + const root: unknown[] = []; + const pending: { readonly value: unknown; readonly output: unknown[] }[] = [ + { value: start, output: root }, + ]; + const appendNode = ( item: Record, uid: number | undefined, - depth: number, - ): unknown[] => { + output: unknown[], + ): void => { const objectId = firstString( authoredId(item), text(item.id), uid === undefined ? null : String(uid), ); - const children = Object.entries(item) - .filter(([key]) => HIERARCHY_KEY.test(key)) - .flatMap(([, child]) => visit(child, depth + 1)); - return objectId === null ? children : [{ objectID: objectId, children }]; + if (objectId === null) { + for (const [key, child] of Object.entries(item).reverse()) + if (HIERARCHY_KEY.test(key)) pending.push({ value: child, output }); + return; + } + const children: unknown[] = []; + output.push({ objectID: objectId, children }); + for (const [key, child] of Object.entries(item).reverse()) + if (HIERARCHY_KEY.test(key)) + pending.push({ value: child, output: children }); }; - const visit = (value: unknown, depth: number): unknown[] => { - if (depth > 32) return []; + while (pending.length > 0) { + const entry = pending.pop(); + if (entry === undefined) break; + const { value, output } = entry; const uid = archiveUid(value); if (uid !== undefined) { - if (visited.has(uid)) return []; + if (visited.has(uid)) continue; visited.add(uid); const target = objectTable[uid]; - if (Array.isArray(target)) return visit(target, depth + 1); - return typeof target === "object" && target !== null - ? node(record(target), uid, depth) - : []; + if (Array.isArray(target)) { + for (let index = target.length - 1; index >= 0; index -= 1) { + const child = target[index]; + if (child !== undefined) pending.push({ value: child, output }); + } + continue; + } + if (typeof target !== "object" || target === null) continue; + const item = record(target); + if (archivedArrayClass(item.$class)) { + const members = item["NS.objects"]; + if (Array.isArray(members)) + for (let index = members.length - 1; index >= 0; index -= 1) { + const child = members[index]; + if (child !== undefined) pending.push({ value: child, output }); + } + continue; + } + appendNode(item, uid, output); + continue; } - if (Array.isArray(value)) - return value.flatMap((item) => visit(item, depth + 1)); - if (typeof value !== "object" || value === null) return []; - return node(record(value), undefined, depth); - }; - return visit(start, 0); + if (Array.isArray(value)) { + for (let index = value.length - 1; index >= 0; index -= 1) { + const child = value[index]; + if (child !== undefined) pending.push({ value: child, output }); + } + } else if (typeof value === "object" && value !== null) { + appendNode(record(value), undefined, output); + } + } + return root; + }; + const archivedArrayClass = (classReference: unknown): boolean => { + const uid = archiveUid(classReference); + const descriptor = record( + uid === undefined ? classReference : objectTable[uid], + ); + const classNames = Array.isArray(descriptor.$classes) + ? descriptor.$classes.filter( + (name): name is string => typeof name === "string", + ) + : []; + return ( + classNames.includes("NSArray") || classNames.includes("NSMutableArray") + ); }; return { resolve, hierarchy }; }; diff --git a/src/domain/apple/keyedArchive.test.ts b/src/domain/apple/keyedArchive.test.ts new file mode 100644 index 0000000000..375729afeb --- /dev/null +++ b/src/domain/apple/keyedArchive.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { MAX_JSON_DEPTH } from "../jsonValue.js"; +import { projectKeyedArchive } from "./keyedArchive.js"; + +const deepValue = (depth: number): unknown => { + let value: unknown = 1; + for (let index = 0; index < depth; index += 1) value = { nested: value }; + return value; +}; + +describe("projectKeyedArchive input bound", () => { + it("rejects an over-deep archive value with a validation error instead of crashing", () => { + let error: unknown; + try { + projectKeyedArchive(deepValue(MAX_JSON_DEPTH + 1), { + offset: 0, + limit: 1, + }); + } catch (caught) { + error = caught; + } + expect(error).toBeInstanceOf(z.ZodError); + expect(error).not.toBeInstanceOf(RangeError); + }); +}); diff --git a/src/domain/apple/keyedArchive.ts b/src/domain/apple/keyedArchive.ts index 2fc6a792f9..0f945b58b3 100644 --- a/src/domain/apple/keyedArchive.ts +++ b/src/domain/apple/keyedArchive.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { AnalysisInputError } from "../analysisErrorCore.js"; import { jsonValueSchema, type JsonValue } from "../jsonValue.js"; /** Select one archive within the active bundle and optionally one named root. */ @@ -85,7 +86,14 @@ export const projectKeyedArchive = ( if (top === undefined || Object.keys(top).length === 0) throw new TypeError("Keyed archive has no $top roots"); if (selection.root !== undefined && !Object.hasOwn(top, selection.root)) - throw new TypeError(`Archive root does not exist: ${selection.root}`); + throw new AnalysisInputError("inspect_keyed_archive", undefined, [ + { + path: ["root"], + reason: "invalid_value", + message: `Archive root does not exist: ${selection.root}. Select one of the archive's $top roots or omit root.`, + expected: Object.keys(top), + }, + ]); const roots = selection.root === undefined ? top diff --git a/src/domain/artifactOperationError.ts b/src/domain/artifactOperationError.ts index 54046be6b8..76c94f348f 100644 --- a/src/domain/artifactOperationError.ts +++ b/src/domain/artifactOperationError.ts @@ -12,6 +12,7 @@ export class ArtifactOperationError extends AnalysisError { | "decode_interface_builder" | "inspect_asset_catalog" | "inspect_keyed_archive" + | "trace_dylib_resolution" | "export_web_scripts" | "trace_web_module_imports" | "trace_web_source_location" diff --git a/src/domain/artifactPathSyntax.test.ts b/src/domain/artifactPathSyntax.test.ts index 7a31567ae5..50eb86c4d4 100644 --- a/src/domain/artifactPathSyntax.test.ts +++ b/src/domain/artifactPathSyntax.test.ts @@ -12,6 +12,11 @@ describe("artifact path syntax", () => { expect(hasScheme("https://example.test/app.js")).toBe(true); expect(hasScheme("file:///app.js")).toBe(true); expect(hasScheme("node:fs")).toBe(true); + expect(hasScheme("web3:app.js")).toBe(true); + expect(hasScheme("h323:user@example.test")).toBe(true); + expect(hasScheme("x1+custom.v2-test:asset")).toBe(true); + expect(hasScheme("3web:app.js")).toBe(false); + expect(hasScheme("./web3:app.js")).toBe(false); expect(hasScheme("./relative.js")).toBe(false); expect(hasScheme("/absolute.js")).toBe(false); expect(hasScheme("app.js")).toBe(false); diff --git a/src/domain/artifactPathSyntax.ts b/src/domain/artifactPathSyntax.ts index 46666dcfb2..382856a4c4 100644 --- a/src/domain/artifactPathSyntax.ts +++ b/src/domain/artifactPathSyntax.ts @@ -17,7 +17,7 @@ /** Whether a reference carries a URI scheme (`https:`, `file:`, …). */ export const hasScheme = (value: string): boolean => - /^[A-Za-z][A-Za-z+.-]*:/u.test(value); + /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value); /** Whether a reference is scheme-qualified or protocol-relative. */ export const looksExternal = (value: string): boolean => diff --git a/src/domain/browserObservation.ts b/src/domain/browserObservation.ts index 7be7809698..b64972a329 100644 --- a/src/domain/browserObservation.ts +++ b/src/domain/browserObservation.ts @@ -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, }); diff --git a/src/domain/browserObservationSchemas.ts b/src/domain/browserObservationSchemas.ts index 84d3349b88..70b53b4a7e 100644 --- a/src/domain/browserObservationSchemas.ts +++ b/src/domain/browserObservationSchemas.ts @@ -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(), diff --git a/src/domain/browserScenario.test.ts b/src/domain/browserScenario.test.ts index c69beeaefd..a5527c4a14 100644 --- a/src/domain/browserScenario.test.ts +++ b/src/domain/browserScenario.test.ts @@ -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: [ diff --git a/src/domain/browserScenarioValues.ts b/src/domain/browserScenarioValues.ts index a2cf1b97c9..6313c7ca9e 100644 --- a/src/domain/browserScenarioValues.ts +++ b/src/domain/browserScenarioValues.ts @@ -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; 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) diff --git a/src/domain/bytecodeProvider.test.ts b/src/domain/bytecodeProvider.test.ts deleted file mode 100644 index d705575216..0000000000 --- a/src/domain/bytecodeProvider.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - analyzeBytecodeArtifacts, - bytecodeAnalysisSchema, - classifyProvenance, - detectArtifactFormat, - detectBytecodeFamily, - type BytecodeArtifact, -} from "./bytecodeProvider.js"; - -describe("bytecode provider", () => { - it("detects JVM family from extensions", () => { - expect(detectBytecodeFamily("App.class")).toBe("jvm"); - expect(detectBytecodeFamily("app.jar")).toBe("jvm"); - expect(detectBytecodeFamily("webapp.war")).toBe("jvm"); - expect(detectBytecodeFamily("app.ear")).toBe("jvm"); - }); - - it("detects Python family from extensions", () => { - expect(detectBytecodeFamily("module.pyc")).toBe("python"); - expect(detectBytecodeFamily("module.pyo")).toBe("python"); - expect(detectBytecodeFamily("package.whl")).toBe("python"); - expect(detectBytecodeFamily("app.pyz")).toBe("python"); - expect(detectBytecodeFamily("app.zipapp")).toBe("python"); - }); - - it("returns null for unknown extensions", () => { - expect(detectBytecodeFamily("readme.txt")).toBeNull(); - expect(detectBytecodeFamily("script.js")).toBeNull(); - }); - - it("detects artifact format from path", () => { - expect(detectArtifactFormat("App.class")).toBe("class"); - expect(detectArtifactFormat("lib.jar")).toBe("jar"); - expect(detectArtifactFormat("web.war")).toBe("war"); - expect(detectArtifactFormat("mod.pyc")).toBe("pyc"); - expect(detectArtifactFormat("pkg.whl")).toBe("whl"); - expect(detectArtifactFormat("unknown.xyz")).toBeNull(); - }); - - it("classifies standard library provenance", () => { - expect(classifyProvenance("/usr/lib/stdlib", "java.lang.String")).toBe( - "standard_library", - ); - expect( - classifyProvenance("/lib/python3.10/site-packages/numpy", "__init__"), - ).toBe("standard_library"); - }); - - it("classifies generated provenance", () => { - expect(classifyProvenance("/build/generated/src", "GeneratedClass")).toBe( - "generated", - ); - expect( - classifyProvenance("/__pycache__/module.cpython-310.pyc", "module"), - ).toBe("generated"); - }); - - it("classifies vendored provenance", () => { - expect(classifyProvenance("/vendor/libs/lib.jar", "VendorLib")).toBe( - "vendored", - ); - expect(classifyProvenance("/third_party/pkg/module.pyc", "module")).toBe( - "vendored", - ); - }); - - it("classifies application provenance", () => { - expect(classifyProvenance("/src/main/App.java", "com.example.App")).toBe( - "application", - ); - expect(classifyProvenance("/src/app.py", "main")).toBe("application"); - }); - - it("analyzes bytecode artifacts and computes summary", () => { - const artifacts: BytecodeArtifact[] = [ - { - path: "src/App.class", - family: "jvm", - format: "class", - size: 1000, - digest: "a".repeat(64), - symbols: [ - { - name: "com.example.App", - raw_location: "0x1000", - kind: "class", - bytecode_version: 52, - provenance: "application", - is_public: true, - }, - { - name: "java.lang.String", - raw_location: "0x2000", - kind: "class", - bytecode_version: 52, - provenance: "standard_library", - is_public: true, - }, - ], - is_standard_library: false, - is_generated: false, - is_vendored: false, - }, - { - path: "lib/generated/Generated.class", - family: "jvm", - format: "class", - size: 500, - digest: "b".repeat(64), - symbols: [ - { - name: "GeneratedClass", - raw_location: "0x3000", - kind: "class", - bytecode_version: 52, - provenance: "generated", - is_public: false, - }, - ], - is_standard_library: false, - is_generated: true, - is_vendored: false, - }, - ]; - const result = analyzeBytecodeArtifacts(artifacts, "jvm"); - expect(result.family).toBe("jvm"); - expect(result.total_symbols).toBe(3); - expect(result.application_symbols).toBe(1); - expect(result.standard_library_symbols).toBe(1); - expect(result.generated_symbols).toBe(1); - expect(result.vendored_symbols).toBe(0); - - // Validate schema - const parsed = bytecodeAnalysisSchema.safeParse(result); - expect(parsed.success).toBe(true); - }); - - it("returns every artifact when the inventory exceeds ten thousand entries", () => { - const artifacts = Array.from({ length: 10_001 }, (_, index) => ({ - path: `classes/${index}.class`, - family: "jvm" as const, - format: "class" as const, - size: 1, - digest: "a".repeat(64), - symbols: [], - is_standard_library: false, - is_generated: false, - is_vendored: false, - })); - - const result = analyzeBytecodeArtifacts(artifacts, "jvm"); - - expect(result.artifacts).toHaveLength(artifacts.length); - expect(bytecodeAnalysisSchema.parse(result).artifacts).toHaveLength( - artifacts.length, - ); - }); -}); diff --git a/src/domain/bytecodeProvider.ts b/src/domain/bytecodeProvider.ts deleted file mode 100644 index cdb56518e9..0000000000 --- a/src/domain/bytecodeProvider.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { z } from "zod"; - -/** Supported managed-runtime bytecode families. */ -export const bytecodeFamilySchema = z.enum(["jvm", "python"]); -export type BytecodeFamily = z.infer; - -/** Classification of bytecode provenance. */ -export const bytecodeProvenanceSchema = z.enum([ - "application", - "generated", - "vendored", - "bundled", - "standard_library", -]); -export type BytecodeProvenance = z.infer; - -/** A bytecode symbol discovered through static analysis. */ -export const bytecodeSymbolSchema = z.strictObject({ - /** Fully qualified name. */ - name: z.string().min(1), - /** Raw bytecode location or offset. */ - raw_location: z.string().min(1), - /** Normalized symbol type. */ - kind: z.enum([ - "class", - "interface", - "method", - "field", - "function", - "module", - "variable", - ]), - /** Bytecode version if known. */ - bytecode_version: z.number().int().nullable(), - /** Provenance classification. */ - provenance: bytecodeProvenanceSchema, - /** Whether the symbol is accessible (public). */ - is_public: z.boolean().default(false), -}); - -/** A discovered bytecode artifact (class file, wheel, etc). */ -export const bytecodeArtifactSchema = z.strictObject({ - /** Artifact path relative to the analysis root. */ - path: z.string().min(1), - /** Bytecode family. */ - family: bytecodeFamilySchema, - /** Artifact format. */ - format: z.enum(["class", "jar", "war", "ear", "pyc", "pyo", "whl", "zipapp"]), - /** Size in bytes. */ - size: z.number().int().nonnegative(), - /** SHA-256 digest. */ - digest: z.string().regex(/^[a-f0-9]{64}$/u), - /** Discovered symbols. */ - symbols: z.array(bytecodeSymbolSchema).default([]), - /** Whether this is a standard library artifact. */ - is_standard_library: z.boolean().default(false), - /** Whether this is a generated artifact. */ - is_generated: z.boolean().default(false), - /** Whether this is a vendored artifact. */ - is_vendored: z.boolean().default(false), -}); -export type BytecodeArtifact = z.infer; - -/** Static bytecode analysis result. */ -export const bytecodeAnalysisSchema = z.strictObject({ - /** Bytecode family. */ - family: bytecodeFamilySchema, - /** All discovered artifacts. */ - artifacts: z.array(bytecodeArtifactSchema), - /** Total symbols across all artifacts. */ - total_symbols: z.number().int().nonnegative(), - /** Symbols classified as application code. */ - application_symbols: z.number().int().nonnegative(), - /** Symbols classified as generated code. */ - generated_symbols: z.number().int().nonnegative(), - /** Symbols classified as vendored code. */ - vendored_symbols: z.number().int().nonnegative(), - /** Symbols classified as standard library. */ - standard_library_symbols: z.number().int().nonnegative(), -}); -export type BytecodeAnalysis = z.infer; - -/** Detect bytecode family from file extension. */ -export function detectBytecodeFamily(path: string): BytecodeFamily | null { - const lower = path.toLowerCase(); - if ( - lower.endsWith(".class") || - lower.endsWith(".jar") || - lower.endsWith(".war") || - lower.endsWith(".ear") - ) { - return "jvm"; - } - if ( - lower.endsWith(".pyc") || - lower.endsWith(".pyo") || - lower.endsWith(".whl") || - lower.endsWith(".pyz") || - lower.endsWith(".zipapp") - ) { - return "python"; - } - return null; -} - -/** Detect artifact format from file path. */ -export function detectArtifactFormat( - path: string, -): `class` | `jar` | `war` | `ear` | `pyc` | `pyo` | `whl` | `zipapp` | null { - const lower = path.toLowerCase(); - if (lower.endsWith(".class")) return "class"; - if (lower.endsWith(".jar")) return "jar"; - if (lower.endsWith(".war")) return "war"; - if (lower.endsWith(".ear")) return "ear"; - if (lower.endsWith(".pyc")) return "pyc"; - if (lower.endsWith(".pyo")) return "pyo"; - if (lower.endsWith(".whl")) return "whl"; - if (lower.endsWith(".pyz") || lower.endsWith(".zipapp")) return "zipapp"; - return null; -} - -const STANDARD_LIBRARY_PATH_MARKERS = [ - "/stdlib/", - "/site-packages/", - "java/", - "javax/", - "sun/", -]; -const STANDARD_LIBRARY_SYMBOL_MARKERS = ["java.", "javax.", "sun.", "__"]; -const GENERATED_PATH_MARKERS = [ - "/generated/", - "_generated", - "/build/", - "/target/", - "/dist-packages/", - "/__pycache__/", -]; -const GENERATED_SYMBOL_MARKERS = ["Generated", "_generated"]; -const VENDORED_PATH_MARKERS = [ - "/vendor/", - "/vendored/", - "/third_party/", - "/3rdparty/", - "/node_modules/", -]; -const BUNDLED_PATH_MARKERS = ["/bundle/", "/bundled/", ".jar!", ".war!"]; - -const includesAny = (value: string, markers: readonly string[]): boolean => - markers.some((marker) => value.includes(marker)); -const startsWithAny = (value: string, markers: readonly string[]): boolean => - markers.some((marker) => value.startsWith(marker)); - -/** Classify bytecode symbol provenance. */ -export function classifyProvenance( - path: string, - symbolName: string, -): BytecodeProvenance { - const lower = path.toLowerCase(); - if ( - includesAny(lower, STANDARD_LIBRARY_PATH_MARKERS) || - startsWithAny(symbolName, STANDARD_LIBRARY_SYMBOL_MARKERS) - ) - return "standard_library"; - if ( - includesAny(lower, GENERATED_PATH_MARKERS) || - includesAny(symbolName, GENERATED_SYMBOL_MARKERS) - ) - return "generated"; - if (includesAny(lower, VENDORED_PATH_MARKERS)) return "vendored"; - if (includesAny(lower, BUNDLED_PATH_MARKERS)) return "bundled"; - return "application"; -} - -/** Analyze a set of bytecode artifacts and produce a summary. */ -export function analyzeBytecodeArtifacts( - artifacts: readonly BytecodeArtifact[], - family: BytecodeFamily, -): BytecodeAnalysis { - const totals = artifacts.reduce( - (counts, artifact) => { - counts.total += artifact.symbols.length; - for (const symbol of artifact.symbols) counts[symbol.provenance] += 1; - return counts; - }, - { - total: 0, - application: 0, - generated: 0, - vendored: 0, - standard_library: 0, - bundled: 0, - }, - ); - - return { - family, - artifacts: [...artifacts], - total_symbols: totals.total, - application_symbols: totals.application, - generated_symbols: totals.generated, - vendored_symbols: totals.vendored, - standard_library_symbols: totals.standard_library, - }; -} diff --git a/src/domain/callPathSchemas.ts b/src/domain/callPathSchemas.ts index 2d964f78fc..130b7177c8 100644 --- a/src/domain/callPathSchemas.ts +++ b/src/domain/callPathSchemas.ts @@ -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(), diff --git a/src/domain/canonicalDigest.test.ts b/src/domain/canonicalDigest.test.ts index 02a641905a..51df946199 100644 --- a/src/domain/canonicalDigest.test.ts +++ b/src/domain/canonicalDigest.test.ts @@ -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 = {}; + 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; diff --git a/src/domain/canonicalDigest.ts b/src/domain/canonicalDigest.ts index c9f1c6260c..3c4f9e3c6a 100644 --- a/src/domain/canonicalDigest.ts +++ b/src/domain/canonicalDigest.ts @@ -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"); }; diff --git a/src/domain/changedBehavior.ts b/src/domain/changedBehavior.ts index ba46372b9d..37cdd97ac7 100644 --- a/src/domain/changedBehavior.ts +++ b/src/domain/changedBehavior.ts @@ -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([]), }); diff --git a/src/domain/comparisonSemantics.test.ts b/src/domain/comparisonSemantics.test.ts index 9e86c0254c..a2c569f6ab 100644 --- a/src/domain/comparisonSemantics.test.ts +++ b/src/domain/comparisonSemantics.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { absenceClaimable, canonicalDigest } from "./comparisonSemantics.js"; +import { absenceClaimable } from "./comparisonSemantics.js"; describe("shared comparison semantics", () => { it("permits absence claims only for complete, non-truncated inventories", () => { @@ -27,10 +27,4 @@ describe("shared comparison semantics", () => { ).toBe(false); expect(absenceClaimable({ status: "partial" })).toBe(false); }); - - it("produces one stable SHA-256 for equivalent canonical JSON values", () => { - expect(canonicalDigest({ b: 2, a: 1 })).toBe( - canonicalDigest({ a: 1, b: 2 }), - ); - }); }); diff --git a/src/domain/comparisonStatus.test.ts b/src/domain/comparisonStatus.test.ts deleted file mode 100644 index a3b8b2e834..0000000000 --- a/src/domain/comparisonStatus.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - bundleClassificationToCanonical, - comparisonConclusionSchema, - comparisonStatusSchema, - functionStatusToCanonical, - scenarioArtifactStatusToCanonical, - sourceToBundleStatusToCanonical, -} from "./comparisonStatus.js"; - -describe("comparison status vocabulary", () => { - it("owns the canonical unchanged/changed/unknown triple", () => { - expect(comparisonStatusSchema.options).toEqual([ - "unchanged", - "changed", - "unknown", - ]); - expect(comparisonConclusionSchema.options).toEqual([ - "observed_change", - "derived_relationship", - "contradiction", - "unresolved_branch", - ]); - }); - - it("projects every bundle classification", () => { - expect( - ( - [ - "added", - "removed", - "changed", - "unknown", - "history_advanced", - "history_diverged", - ] as const - ).map(bundleClassificationToCanonical), - ).toEqual([ - "changed", - "changed", - "changed", - "unknown", - "changed", - "changed", - ]); - }); - - it("projects every source-to-bundle item status", () => { - expect( - ( - [ - "unchanged", - "modified", - "removed", - "split", - "merged", - "duplicated", - "unknown", - ] as const - ).map(sourceToBundleStatusToCanonical), - ).toEqual([ - "unchanged", - "changed", - "changed", - "changed", - "changed", - "changed", - "unknown", - ]); - }); - - it("projects every function dimension status without claiming truncated work", () => { - expect( - (["unchanged", "changed", "truncated", "unknown"] as const).map( - functionStatusToCanonical, - ), - ).toEqual(["unchanged", "changed", "unknown", "unknown"]); - }); - - it("projects every scenario artifact status", () => { - expect( - (["changed", "unknown"] as const).map(scenarioArtifactStatusToCanonical), - ).toEqual(["changed", "unknown"]); - }); -}); diff --git a/src/domain/comparisonStatus.ts b/src/domain/comparisonStatus.ts index 2674c24c86..b23c90b1cd 100644 --- a/src/domain/comparisonStatus.ts +++ b/src/domain/comparisonStatus.ts @@ -2,69 +2,11 @@ import { z } from "zod"; /** * Canonical top-level comparison outcome shared by every comparison - * pipeline. Pipeline-native detail enums (bundle classifications, - * source-to-bundle split/merge kinds, scenario alignment states, function - * truncation) keep their wire schemas; the mappers below project each of - * them onto this triple without changing any pipeline result. + * pipeline. Pipeline-native detail enums keep their wire schemas, while + * shared result contracts use this canonical triple. */ export const comparisonStatusSchema = z.enum([ "unchanged", "changed", "unknown", ]); - -export type ComparisonStatus = z.infer; - -/** Canonical comparison conclusion shared by every comparison pipeline. */ -export const comparisonConclusionSchema = z.enum([ - "observed_change", - "derived_relationship", - "contradiction", - "unresolved_branch", -]); - -/** Project a bundle change classification onto the canonical triple. */ -export const bundleClassificationToCanonical = ( - classification: - | "added" - | "removed" - | "changed" - | "unknown" - | "history_advanced" - | "history_diverged", -): ComparisonStatus => (classification === "unknown" ? "unknown" : "changed"); - -/** Project a source-to-bundle item status onto the canonical triple. */ -export const sourceToBundleStatusToCanonical = ( - status: - | "unchanged" - | "modified" - | "removed" - | "split" - | "merged" - | "duplicated" - | "unknown", -): ComparisonStatus => - status === "unchanged" - ? "unchanged" - : status === "unknown" - ? "unknown" - : "changed"; - -/** - * Project a function dimension status onto the canonical triple. A - * truncated dimension cannot support an unchanged/changed claim. - */ -export const functionStatusToCanonical = ( - status: "unchanged" | "changed" | "truncated" | "unknown", -): ComparisonStatus => - status === "unchanged" - ? "unchanged" - : status === "changed" - ? "changed" - : "unknown"; - -/** Project a browser-scenario artifact status onto the canonical triple. */ -export const scenarioArtifactStatusToCanonical = ( - status: "changed" | "unknown", -): ComparisonStatus => status; diff --git a/src/domain/conformanceTrustGate.ts b/src/domain/conformanceTrustGate.ts index 0b3e02b1cb..14116f949b 100644 --- a/src/domain/conformanceTrustGate.ts +++ b/src/domain/conformanceTrustGate.ts @@ -51,19 +51,6 @@ const VOLATILE_DIMENSIONS = new Set([ "updated_at", ]); -/** Dimension that carries semantic content. */ -const SEMANTIC_DIMENSIONS = new Set([ - "exit_code", - "stdout", - "stderr", - "filesystem", - "process", - "process_tree", - "shim_events", - "protocol_events", - "event_journal", -]); - type IncompleteStatus = "unknown" | "truncated"; type EvidenceView = { @@ -214,11 +201,6 @@ export function isVolatileDimension(name: string): boolean { return VOLATILE_DIMENSIONS.has(name); } -/** Check if a dimension name carries semantic content. */ -export function isSemanticDimension(name: string): boolean { - return SEMANTIC_DIMENSIONS.has(name); -} - /** * Compare two values for a dimension, classifying semantic diffs * from timing/normalization noise. diff --git a/src/domain/customProtocolCapture.test.ts b/src/domain/customProtocolCapture.test.ts deleted file mode 100644 index b39b880ca7..0000000000 --- a/src/domain/customProtocolCapture.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { fc } from "@fast-check/vitest"; -import { describe, expect, it } from "vitest"; - -import { - authEventsByProtocol, - authFlowDuration, - customProtocolCaptureSchema, - correlateProcessIdentity, - failedAuthEvents, - framesByTransport, - looksLikeCredentialFrame, - successfulAuthEvents, - type AuthFlowEvent, - type ProtocolFrame, -} from "./customProtocolCapture.js"; - -const sampleFrames: ProtocolFrame[] = [ - { - sequence: 0, - at_ms: 100, - transport: "tcp", - direction: "sent", - source_pid: 100, - dest_pid: 200, - source_endpoint: "127.0.0.1:8080", - dest_endpoint: "127.0.0.1:9090", - raw_data: "hello", - decoded_content: "hello", - size: 5, - truncated: false, - }, - { - sequence: 1, - at_ms: 200, - transport: "udp", - direction: "received", - source_pid: 200, - dest_pid: 100, - source_endpoint: "127.0.0.1:9090", - dest_endpoint: "127.0.0.1:8080", - raw_data: "world", - decoded_content: "world", - size: 5, - truncated: false, - }, -]; - -const sampleAuthEvents: AuthFlowEvent[] = [ - { - sequence: 0, - at_ms: 100, - stage: "init", - protocol: "OAuth2", - token_type: "Bearer", - token_lifecycle: "issued", - correlation_id: "abc-123", - credentials_redacted: true, - succeeded: false, - error: null, - }, - { - sequence: 1, - at_ms: 500, - stage: "success", - protocol: "OAuth2", - token_type: "Bearer", - token_lifecycle: "issued", - correlation_id: "abc-123", - credentials_redacted: true, - succeeded: true, - error: null, - }, -]; - -describe("custom protocol capture", () => { - it("rejects a capture with an unknown transport", () => { - const result = customProtocolCaptureSchema.safeParse({ - transport: "carrier-pigeon", - frames: sampleFrames, - auth_events: sampleAuthEvents, - has_truncated: false, - credentials_detected: false, - }); - expect(result.success).toBe(false); - }); - - it("retains every captured frame at any size", () => { - fc.assert( - fc.property( - // The bound must sit well above any ceiling a future change would - // plausibly introduce. A generated length of 40 would pass unnoticed - // against a cap at 100, so the generated arrays cross 1,000. - fc.array(fc.constant(sampleFrames[0]!), { - minLength: 300, - maxLength: 1_000, - }), - (frames) => { - const parsed = customProtocolCaptureSchema.parse({ - transport: "tcp", - frames: frames.map((frame, sequence) => ({ ...frame, sequence })), - }); - expect(parsed.frames).toHaveLength(frames.length); - }, - ), - { numRuns: 25 }, - ); - }); - - it("correlates process identity", () => { - expect(correlateProcessIdentity(sampleFrames[0]!, 100)).toBe(true); - expect(correlateProcessIdentity(sampleFrames[0]!, 200)).toBe(true); - expect(correlateProcessIdentity(sampleFrames[0]!, 999)).toBe(false); - }); - - it("filters frames by transport", () => { - const tcpFrames = framesByTransport(sampleFrames, "tcp"); - expect(tcpFrames).toHaveLength(1); - const udpFrames = framesByTransport(sampleFrames, "udp"); - expect(udpFrames).toHaveLength(1); - }); - - it("detects credential-like frames", () => { - const credFrame: ProtocolFrame = { - ...sampleFrames[0]!, - decoded_content: { password: "secret" }, - }; - expect(looksLikeCredentialFrame(credFrame)).toBe(true); - expect(looksLikeCredentialFrame(sampleFrames[0]!)).toBe(false); - }); - - it("filters auth events by protocol", () => { - const oauthEvents = authEventsByProtocol(sampleAuthEvents, "OAuth2"); - expect(oauthEvents).toHaveLength(2); - const otherEvents = authEventsByProtocol(sampleAuthEvents, "SAML"); - expect(otherEvents).toHaveLength(0); - }); - - it("gets successful auth events", () => { - const success = successfulAuthEvents(sampleAuthEvents); - expect(success).toHaveLength(1); - expect(success[0]!.succeeded).toBe(true); - }); - - it("gets failed auth events", () => { - const failed = failedAuthEvents(sampleAuthEvents); - expect(failed).toHaveLength(1); - expect(failed[0]!.succeeded).toBe(false); - }); - - it("computes auth flow duration", () => { - const duration = authFlowDuration(sampleAuthEvents); - expect(duration).toBe(400); - }); - - it("returns null for empty auth events", () => { - expect(authFlowDuration([])).toBeNull(); - }); -}); diff --git a/src/domain/customProtocolCapture.ts b/src/domain/customProtocolCapture.ts deleted file mode 100644 index 0b0989237f..0000000000 --- a/src/domain/customProtocolCapture.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { z } from "zod"; - -import { jsonValueSchema } from "./jsonValue.js"; - -/** Supported transport types for custom protocol capture. */ -export const transportTypeSchema = z.enum([ - "tcp", - "udp", - "ipc", - "unix-socket", - "named-pipe", - "xpc", -]); -export type TransportType = z.infer; - -/** Direction of a protocol frame. */ -export const frameDirectionSchema = z.enum(["sent", "received", "intercepted"]); - -/** A captured protocol frame. */ -export const protocolFrameSchema = z.strictObject({ - /** Monotonic sequence number. */ - sequence: z.number().int().nonnegative(), - /** Timestamp in milliseconds. */ - at_ms: z.number().int().nonnegative(), - /** Transport type. */ - transport: transportTypeSchema, - /** Direction of the frame. */ - direction: frameDirectionSchema, - /** Source process identity. */ - source_pid: z.number().int().nullable(), - /** Destination process identity. */ - dest_pid: z.number().int().nullable(), - /** Endpoint address (IP:port, socket path, pipe name). */ - source_endpoint: z.string().nullable(), - /** Destination endpoint. */ - dest_endpoint: z.string().nullable(), - /** Raw frame data as base64. */ - raw_data: z.string().nullable(), - /** Decoded/parsed frame content if known. */ - decoded_content: jsonValueSchema.nullable(), - /** Frame size in bytes. */ - size: z.number().int().nonnegative(), - /** Whether the frame was truncated. */ - truncated: z.boolean().default(false), -}); -export type ProtocolFrame = z.infer; - -/** Authentication flow stage. */ -export const authStageSchema = z.enum([ - "init", - "challenge", - "response", - "token_exchange", - "renewal", - "revocation", - "failure", - "success", -]); - -/** Token lifecycle event. */ -export const tokenLifecycleEventSchema = z.enum([ - "issued", - "refreshed", - "expired", - "revoked", - "renewed", -]); - -/** A captured authentication flow event. */ -export const authFlowEventSchema = z.strictObject({ - /** Monotonic sequence number. */ - sequence: z.number().int().nonnegative(), - /** Timestamp in milliseconds. */ - at_ms: z.number().int().nonnegative(), - /** Authentication stage. */ - stage: authStageSchema, - /** Protocol used. */ - protocol: z.string().min(1), - /** Token type if applicable. */ - token_type: z.string().nullable(), - /** Token lifecycle event. */ - token_lifecycle: tokenLifecycleEventSchema.nullable(), - /** Correlation ID linking challenge-response pairs. */ - correlation_id: z.string().nullable(), - /** Whether the capture producer marked credentials as redacted. */ - credentials_redacted: z.boolean().default(false), - /** Whether the authentication succeeded. */ - succeeded: z.boolean().default(false), - /** Error message if authentication failed. */ - error: z.string().nullable(), -}); -export type AuthFlowEvent = z.infer; - -/** A captured protocol session. */ -export const customProtocolCaptureSchema = z.strictObject({ - /** Transport type for this session. */ - transport: transportTypeSchema, - /** Captured frames in order. */ - frames: z.array(protocolFrameSchema), - /** Authentication flow events. */ - auth_events: z.array(authFlowEventSchema).default([]), - /** Whether any frame was truncated. */ - has_truncated: z.boolean().default(false), - /** Whether the capture producer detected credential content. */ - credentials_detected: z.boolean().default(false), -}); - -/** Correlate a frame with process identity. */ -export function correlateProcessIdentity( - frame: ProtocolFrame, - pid: number, -): boolean { - return frame.source_pid === pid || frame.dest_pid === pid; -} - -/** Check if a frame contains credential-like content. */ -export function looksLikeCredentialFrame(frame: ProtocolFrame): boolean { - const patterns = [ - "password", - "token", - "secret", - "api_key", - "apikey", - "authorization", - "credential", - ]; - const content = JSON.stringify(frame.decoded_content ?? "").toLowerCase(); - return patterns.some((p) => content.includes(p)); -} - -/** Filter frames by transport type. */ -export function framesByTransport( - frames: readonly ProtocolFrame[], - transport: TransportType, -): ProtocolFrame[] { - return frames.filter((f) => f.transport === transport); -} - -/** Get all authentication flow events for a specific protocol. */ -export function authEventsByProtocol( - events: readonly AuthFlowEvent[], - protocol: string, -): AuthFlowEvent[] { - return events.filter((e) => e.protocol === protocol); -} - -/** Get successful authentication events. */ -export function successfulAuthEvents( - events: readonly AuthFlowEvent[], -): AuthFlowEvent[] { - return events.filter((e) => e.succeeded); -} - -/** Get failed authentication events. */ -export function failedAuthEvents( - events: readonly AuthFlowEvent[], -): AuthFlowEvent[] { - return events.filter((e) => !e.succeeded); -} - -/** Compute the duration of an authentication flow in milliseconds. */ -export function authFlowDuration( - events: readonly AuthFlowEvent[], -): number | null { - const first = events.at(0); - const last = events.at(-1); - if (first === undefined || last === undefined) return null; - return last.at_ms - first.at_ms; -} diff --git a/src/domain/digests.test.ts b/src/domain/digests.test.ts index 9593cef6ff..7beb708816 100644 --- a/src/domain/digests.test.ts +++ b/src/domain/digests.test.ts @@ -5,7 +5,6 @@ import { isDigest, isPrefixedDigest, prefixedDigestSchema, - SHA256_PATTERN, } from "./digests.js"; const DIGEST = "a".repeat(64); @@ -59,9 +58,4 @@ describe("digest shapes", () => { `jag_${OTHER_DIGEST}`, ); }); - - it("exposes the pattern for callers that cannot use zod", () => { - expect(SHA256_PATTERN.test(DIGEST)).toBe(true); - expect(SHA256_PATTERN.test(DIGEST.toUpperCase())).toBe(false); - }); }); diff --git a/src/domain/emptyArraySchema.test.ts b/src/domain/emptyArraySchema.test.ts deleted file mode 100644 index c2cf8a3dae..0000000000 --- a/src/domain/emptyArraySchema.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { emptyArraySchema } from "./emptyArraySchema.js"; - -describe("empty array schema", () => { - it("accepts only an empty array", () => { - expect(emptyArraySchema.parse([])).toEqual([]); - for (const value of [ - [null], - ["candidate"], - [0], - [false], - [{}], - [[]], - null, - {}, - ]) { - expect(emptyArraySchema.safeParse(value).success).toBe(false); - } - }); -}); diff --git a/src/domain/eventProcessTree.fixture.ts b/src/domain/eventProcessTree.fixture.ts deleted file mode 100644 index 77368a9ac2..0000000000 --- a/src/domain/eventProcessTree.fixture.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { ProcessNode, ProcessTreeEvent } from "./eventProcessTree.js"; - -/** - * Defaults shared by every fixture event. Tests vary only the fields they are - * actually asserting on, so a tree reads as the sequence of transitions under - * test rather than thirteen repeated literal fields per event. - */ -const eventDefaults: { - arguments: string[]; - previous_pid: number | null; - previous_ppid: number | null; - exit_code: number | null; - signal: number | null; - tid: number | null; -} = { - arguments: [], - previous_pid: null, - previous_ppid: null, - exit_code: null, - signal: null, - tid: null, -}; - -/** One `spawn` event, numbering itself from the sequence given. */ -export const spawnEvent = (event: { - sequence: number; - at_ms?: number; - pid: number; - ppid: number; - process_name: string; - executable?: string | null; - arguments?: string[]; -}): ProcessTreeEvent => ({ - ...eventDefaults, - at_ms: event.at_ms ?? event.sequence * 10, - type: "spawn", - executable: event.executable ?? `/${event.process_name}`, - ...event, -}); - -/** One `exit` event. */ -export const exitEvent = (event: { - sequence: number; - at_ms?: number; - pid: number; - ppid: number | null; - process_name: string | null; - exit_code?: number; -}): ProcessTreeEvent => ({ - ...eventDefaults, - at_ms: event.at_ms ?? event.sequence * 10, - type: "exit", - executable: null, - exit_code: 0, - ...event, -}); - -/** One `re_exec` event, which keeps the pid and replaces the executable. */ -export const reExecEvent = (event: { - sequence: number; - at_ms?: number; - pid: number; - ppid: number; - process_name: string; - executable: string; - arguments?: string[]; -}): ProcessTreeEvent => ({ - ...eventDefaults, - at_ms: event.at_ms ?? event.sequence * 10, - type: "re_exec", - previous_pid: null, - ...event, -}); - -/** One `reparent` event, which records the pid the process was adopted from. */ -export const reparentEvent = (event: { - sequence: number; - at_ms?: number; - pid: number; - ppid: number; - process_name: string; - previous_ppid: number; -}): ProcessTreeEvent => ({ - ...eventDefaults, - at_ms: event.at_ms ?? event.sequence * 10, - type: "reparent", - executable: null, - ...event, -}); - -/** A reconstructed node, defaulting to a childless process with no lifecycle. */ -export const processNode = (node: { - pid: number; - ppid: number | null; - process_name: string; - children?: number[]; -}): ProcessNode => ({ - executable: null, - arguments: [], - spawn_time_ms: null, - exit_time_ms: null, - exit_code: null, - is_re_exec: false, - is_reparented: false, - previous_ppid: null, - ...node, - children: node.children ?? [], -}); - -/** The common `spawn` of a root process at pid 1. */ -export const rootSpawn = (sequence = 0): ProcessTreeEvent => - spawnEvent({ sequence, pid: 1, ppid: 0, process_name: "root" }); diff --git a/src/domain/eventProcessTree.test.ts b/src/domain/eventProcessTree.test.ts deleted file mode 100644 index 1796421809..0000000000 --- a/src/domain/eventProcessTree.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - containsProcess, - getDescendants, - maxTreeDepth, - reconstructProcessTree, - type ProcessTreeEvent, -} from "./eventProcessTree.js"; -import { - exitEvent, - processNode, - reExecEvent, - reparentEvent, - rootSpawn, - spawnEvent, -} from "./eventProcessTree.fixture.js"; - -describe("event-backed process tree reconstruction", () => { - it("walks trees larger than ten thousand nodes", () => { - const childPids = Array.from({ length: 10_001 }, (_, index) => index + 2); - const root = processNode({ - pid: 1, - ppid: null, - process_name: "root", - children: childPids, - }); - const children = childPids.map((pid) => - processNode({ pid, ppid: 1, process_name: "root" }), - ); - - expect(maxTreeDepth([root, ...children], 1)).toBe(1); - expect(getDescendants([root, ...children], 1)).toHaveLength( - childPids.length, - ); - }); - - it("reconstructs a simple process tree", () => { - const events: ProcessTreeEvent[] = [ - rootSpawn(0), - spawnEvent({ - sequence: 1, - pid: 2, - ppid: 1, - process_name: "child", - executable: "/child", - }), - exitEvent({ - sequence: 2, - at_ms: 20, - pid: 2, - ppid: 1, - process_name: "child", - }), - exitEvent({ - sequence: 3, - at_ms: 30, - pid: 1, - ppid: 0, - process_name: "root", - }), - ]; - const result = reconstructProcessTree(events, 1); - expect(result.processes).toHaveLength(2); - expect(result.events_consumed).toBe(4); - expect(result.events_unmatched).toBe(0); - expect(result.short_lived_descendants).toBe(2); - }); -}); - -describe("short-lived process descendants", () => { - it("detects short-lived descendants", () => { - const events: ProcessTreeEvent[] = [ - rootSpawn(0), - spawnEvent({ - sequence: 1, - pid: 2, - ppid: 1, - process_name: "short", - executable: "/short", - }), - exitEvent({ - sequence: 2, - at_ms: 11, - pid: 2, - ppid: 1, - process_name: "short", - }), - ]; - const result = reconstructProcessTree(events, 1); - expect(result.short_lived_descendants).toBe(1); - }); -}); - -describe("event-backed process tree transitions", () => { - it("detects re-exec", () => { - const events: ProcessTreeEvent[] = [ - rootSpawn(0), - reExecEvent({ - sequence: 1, - pid: 1, - ppid: 0, - process_name: "root", - executable: "/newroot", - arguments: ["--new"], - }), - ]; - const result = reconstructProcessTree(events, 1); - expect(result.has_re_exec).toBe(true); - const root = result.processes.find((p) => p.pid === 1); - expect(root?.is_re_exec).toBe(true); - expect(root?.executable).toBe("/newroot"); - }); - - it("detects reparenting", () => { - const events: ProcessTreeEvent[] = [ - rootSpawn(0), - spawnEvent({ - sequence: 1, - pid: 2, - ppid: 1, - process_name: "child", - executable: "/child", - }), - reparentEvent({ - sequence: 2, - pid: 2, - ppid: 0, - process_name: "child", - previous_ppid: 1, - }), - ]; - const result = reconstructProcessTree(events, 1); - expect(result.has_reparenting).toBe(true); - const child = result.processes.find((p) => p.pid === 2); - expect(child?.is_reparented).toBe(true); - expect(child?.previous_ppid).toBe(1); - expect(child?.ppid).toBe(0); - }); - - it("reports unmatched events for unknown exits", () => { - const events: ProcessTreeEvent[] = [ - exitEvent({ - sequence: 0, - at_ms: 0, - pid: 999, - ppid: null, - process_name: null, - exit_code: 1, - }), - ]; - const result = reconstructProcessTree(events, 1); - expect(result.events_unmatched).toBe(1); - expect(result.processes).toHaveLength(0); - }); -}); - -describe("event-backed process tree queries", () => { - it("gets descendants", () => { - const events: ProcessTreeEvent[] = [ - rootSpawn(0), - spawnEvent({ - sequence: 1, - pid: 2, - ppid: 1, - process_name: "child1", - executable: "/c1", - }), - spawnEvent({ - sequence: 2, - pid: 3, - ppid: 2, - process_name: "grandchild", - executable: "/gc", - }), - ]; - const result = reconstructProcessTree(events, 1); - const descendants = getDescendants(result.processes, 1); - expect(descendants).toHaveLength(2); - expect(descendants).toContain(2); - expect(descendants).toContain(3); - }); - - it("checks process containment", () => { - const events: ProcessTreeEvent[] = [rootSpawn(0)]; - const result = reconstructProcessTree(events, 1); - expect(containsProcess(result.processes, 1)).toBe(true); - expect(containsProcess(result.processes, 999)).toBe(false); - }); -}); - -describe("event-backed process tree depth and types", () => { - it("computes max tree depth", () => { - const events: ProcessTreeEvent[] = [ - rootSpawn(0), - spawnEvent({ - sequence: 1, - pid: 2, - ppid: 1, - process_name: "child", - executable: "/c", - }), - spawnEvent({ - sequence: 2, - pid: 3, - ppid: 2, - process_name: "gc", - executable: "/gc", - }), - ]; - const result = reconstructProcessTree(events, 1); - expect(maxTreeDepth(result.processes, 1)).toBe(2); - }); -}); diff --git a/src/domain/eventProcessTree.ts b/src/domain/eventProcessTree.ts deleted file mode 100644 index ad04efd764..0000000000 --- a/src/domain/eventProcessTree.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { z } from "zod"; - -/** Event types for event-backed process-tree capture. */ -export const processEventTypeSchema = z.enum([ - "spawn", - "exit", - "re_exec", - "reparent", - "signal", - "name_change", - "thread_create", - "thread_exit", -]); - -/** A single event-backed process-tree event. */ -export const processTreeEventSchema = z.strictObject({ - /** Monotonic event sequence number. */ - sequence: z.number().int().nonnegative(), - /** Event timestamp in milliseconds. */ - at_ms: z.number().int().nonnegative(), - /** Event type. */ - type: processEventTypeSchema, - /** Process ID. */ - pid: z.number().int().positive(), - /** Parent process ID. */ - ppid: z.number().int().nullable(), - /** Process name if known. */ - process_name: z.string().nullable(), - /** Executable path if known. */ - executable: z.string().nullable(), - /** Command line arguments. */ - arguments: z.array(z.string()).default([]), - /** Previous PID (for reparent events). */ - previous_pid: z.number().int().nullable(), - /** Previous PPID (for reparent events). */ - previous_ppid: z.number().int().nullable(), - /** Exit code (for exit events). */ - exit_code: z.number().int().nullable(), - /** Signal number (for signal events). */ - signal: z.number().int().nullable(), - /** Thread ID (for thread events). */ - tid: z.number().int().nullable(), -}); -export type ProcessTreeEvent = z.infer; - -/** Process node in a reconstructed tree. */ -export const processNodeSchema = z.strictObject({ - pid: z.number().int().positive(), - ppid: z.number().int().nullable(), - process_name: z.string().nullable(), - executable: z.string().nullable(), - arguments: z.array(z.string()).default([]), - children: z.array(z.number().int()).default([]), - spawn_time_ms: z.number().int().nullable(), - exit_time_ms: z.number().int().nullable(), - exit_code: z.number().int().nullable(), - is_re_exec: z.boolean().default(false), - is_reparented: z.boolean().default(false), - previous_ppid: z.number().int().nullable(), -}); -export type ProcessNode = z.infer; - -/** Result of event-backed process-tree reconstruction. */ -export const processTreeReconstructionSchema = z.strictObject({ - /** Root PID of the process tree. */ - root_pid: z.number().int().positive(), - /** All reconstructed process nodes. */ - processes: z.array(processNodeSchema).min(0), - /** Events that were consumed. */ - events_consumed: z.number().int().nonnegative(), - /** Events that could not be matched. */ - events_unmatched: z.number().int().nonnegative(), - /** Short-lived descendants that were missed by sampling. */ - short_lived_descendants: z.number().int().nonnegative(), - /** Whether any re-exec was detected. */ - has_re_exec: z.boolean().default(false), - /** Whether any reparenting was detected. */ - has_reparenting: z.boolean().default(false), -}); -export type ProcessTreeReconstruction = z.infer< - typeof processTreeReconstructionSchema ->; - -type ProcessTreeAccumulator = { - readonly nodes: Map; - readonly children: Map; - eventsUnmatched: number; - hasReExec: boolean; - hasReparenting: boolean; - shortLivedDescendants: number; -}; - -const applyProcessTreeEvent = ( - event: ProcessTreeEvent, - state: ProcessTreeAccumulator, -): void => { - let node = state.nodes.get(event.pid); - switch (event.type) { - case "spawn": { - if (!node) { - node = { - pid: event.pid, - ppid: event.ppid, - process_name: event.process_name, - executable: event.executable, - arguments: event.arguments, - children: [], - spawn_time_ms: event.at_ms, - exit_time_ms: null, - exit_code: null, - is_re_exec: false, - is_reparented: false, - previous_ppid: null, - }; - state.nodes.set(event.pid, node); - } - if (event.ppid !== null) { - const parentChildren = state.children.get(event.ppid) ?? []; - parentChildren.push(event.pid); - state.children.set(event.ppid, parentChildren); - } - break; - } - case "exit": - if (node) { - node.exit_time_ms = event.at_ms; - node.exit_code = event.exit_code; - if ( - node.spawn_time_ms !== null && - event.at_ms - node.spawn_time_ms < 100 - ) - state.shortLivedDescendants++; - } else state.eventsUnmatched++; - break; - case "re_exec": - if (node) { - node.is_re_exec = true; - node.executable = event.executable ?? node.executable; - node.arguments = event.arguments; - state.hasReExec = true; - } else state.eventsUnmatched++; - break; - case "reparent": - if (node) { - node.previous_ppid = node.ppid; - node.ppid = event.ppid; - node.is_reparented = true; - state.hasReparenting = true; - } else state.eventsUnmatched++; - break; - case "signal": - case "name_change": - case "thread_create": - case "thread_exit": - // These events are recorded but don't modify the tree structure. - break; - } -}; - -/** Reconstruct a process tree from event-backed capture. */ -export function reconstructProcessTree( - events: readonly ProcessTreeEvent[], - rootPid: number, -): ProcessTreeReconstruction { - const nodes = new Map(); - const children = new Map(); - let eventsConsumed = 0; - const state: ProcessTreeAccumulator = { - nodes, - children, - eventsUnmatched: 0, - hasReExec: false, - hasReparenting: false, - shortLivedDescendants: 0, - }; - - const sorted = [...events].sort((a, b) => a.at_ms - b.at_ms); - - for (const event of sorted) { - eventsConsumed++; - applyProcessTreeEvent(event, state); - } - - // Attach children to nodes - for (const [pid, childPids] of children) { - const node = nodes.get(pid); - if (node) node.children = childPids; - } - - return { - root_pid: rootPid, - processes: [...nodes.values()], - events_consumed: eventsConsumed, - events_unmatched: state.eventsUnmatched, - short_lived_descendants: state.shortLivedDescendants, - has_re_exec: state.hasReExec, - has_reparenting: state.hasReparenting, - }; -} - -/** Get all descendant PIDs of a given root PID. */ -export function getDescendants( - processes: readonly ProcessNode[], - rootPid: number, -): number[] { - const byPid = new Map(processes.map((p) => [p.pid, p])); - const visited = new Set(); - const result: number[] = []; - const queue: number[] = [rootPid]; - for (let head = 0; head < queue.length; head += 1) { - const pid = queue[head]; - if (pid === undefined) continue; - if (visited.has(pid)) continue; - visited.add(pid); - const node = byPid.get(pid); - if (node) { - for (const child of node.children) { - if (!visited.has(child)) { - result.push(child); - queue.push(child); - } - } - } - } - return result; -} - -/** Check if a process tree contains a specific PID. */ -export function containsProcess( - processes: readonly ProcessNode[], - pid: number, -): boolean { - return processes.some((p) => p.pid === pid); -} - -/** Get the maximum tree depth from the root. */ -export function maxTreeDepth( - processes: readonly ProcessNode[], - rootPid: number, -): number { - const byPid = new Map(processes.map((p) => [p.pid, p])); - const pending = [{ pid: rootPid, depth: 0 }]; - const visited = new Set(); - let maximumDepth = 0; - - while (pending.length > 0) { - const current = pending.pop(); - if (!current || visited.has(current.pid)) continue; - visited.add(current.pid); - - const node = byPid.get(current.pid); - if (!node || node.children.length === 0) continue; - - const childDepth = current.depth + 1; - maximumDepth = Math.max(maximumDepth, childDepth); - for (const childPid of node.children) - if (!visited.has(childPid)) - pending.push({ pid: childPid, depth: childDepth }); - } - - return maximumDepth; -} diff --git a/src/domain/evidence.test.ts b/src/domain/evidence.test.ts index c136637090..95dabcc1a9 100644 --- a/src/domain/evidence.test.ts +++ b/src/domain/evidence.test.ts @@ -4,6 +4,7 @@ import { describe, expect } from "vitest"; import { createAnalysisProfile } from "./analysisProfile.js"; import type { BinaryTarget } from "./binaryTarget.js"; import { createEvidence, evidenceSchema, parseEvidence } from "./evidence.js"; +import { MAX_JSON_DEPTH } from "./jsonValue.js"; import { createEvidenceBundle } from "./evidenceBundle.js"; const TARGET: BinaryTarget = { @@ -17,14 +18,53 @@ const TARGET: BinaryTarget = { const PROVIDER = { id: "fixture", name: "Fixture provider", version: "1" }; const PROFILE = createAnalysisProfile(PROVIDER, { loader: "default" }); +it("snapshots caller-owned payloads before deriving their identity", () => { + const result = { nested: { values: ["observed"] } }; + const rawResult = { entries: [{ value: "raw" }] }; + const parameters = { selected: ["target"] }; + const evidence = createEvidence(TARGET, PROVIDER, { + operation: "inspect", + parameters, + result, + rawResult, + }); + result.nested.values.push("later"); + const rawEntry = rawResult.entries[0]; + if (rawEntry !== undefined) rawEntry.value = "changed"; + parameters.selected.push("other"); + expect(evidence.normalized_result).toEqual({ + nested: { values: ["observed"] }, + }); + expect(evidence.raw_result).toEqual({ entries: [{ value: "raw" }] }); + expect(evidence.parameters).toEqual({ selected: ["target"] }); + expect(parseEvidence(evidence)).toEqual(evidence); + expect(() => + parseEvidence({ ...evidence, normalized_result: result }), + ).toThrow(/semantic identifier/u); +}); + describe("analysis evidence identity", () => { - it("normalizes prototype-named parameter keys", () => { + it("preserves prototype-named parameter keys and their semantic identity", () => { const evidence = createEvidence(TARGET, PROVIDER, { operation: "health", parameters: Object.fromEntries([["__proto__", false]]), result: true, }); + expect(evidence.parameters).toEqual({ ["__proto__"]: false }); + expect(Object.hasOwn(evidence.parameters, "__proto__")).toBe(true); expect(parseEvidence(evidence)).toEqual(evidence); + const stripped = createEvidence(TARGET, PROVIDER, { + operation: "health", + parameters: {}, + result: true, + }); + expect(stripped.evidence_id).not.toBe(evidence.evidence_id); + expect(() => + parseEvidence({ + ...evidence, + parameters: { ["__proto__"]: true }, + }), + ).toThrow(/semantic identifier/u); }); it.prop([ @@ -156,6 +196,23 @@ describe("analysis evidence identity", () => { }); }); +it("binds an explicit unknown-subject reason into the semantic Evidence identity", () => { + const reason = "Caller excluded the observed digest."; + const observation = { operation: "health", parameters: {}, result: true }; + const explicit = createEvidence(undefined, PROVIDER, { + ...observation, + subjectUnavailableReason: reason, + }); + const defaulted = createEvidence(undefined, PROVIDER, observation); + expect(explicit.subject).toBeNull(); + expect(explicit.limitations).toEqual([reason]); + expect(defaulted.limitations).toEqual([ + "Artifact identity is unavailable for this observation.", + ]); + expect(explicit.evidence_id).not.toBe(defaulted.evidence_id); + expect(parseEvidence(explicit)).toEqual(explicit); +}); + it("derives byte-stable bundle manifests independent of record order", () => { const artifactEvidence = createEvidence(TARGET, PROVIDER, { operation: "health", @@ -213,3 +270,51 @@ describe("DOS analysis evidence identity", () => { ); }); }); + +describe("evidence parameter depth bound", () => { + it("rejects evidence whose parameters nest past the JSON depth limit", () => { + const evidence = createEvidence(TARGET, PROVIDER, { + operation: "health", + parameters: {}, + result: true, + }); + let value: unknown = 1; + for (let index = 0; index <= MAX_JSON_DEPTH; index += 1) + value = { nested: value }; + const result = evidenceSchema.safeParse({ + ...evidence, + parameters: { attack: value }, + }); + expect(result.success).toBe(false); + if (result.success) return; + expect( + result.error.issues.some((issue) => + issue.message.includes("maximum nesting depth"), + ), + ).toBe(true); + }); +}); + +it("preserves prototype-named raw and normalized results with aligned identity", () => { + const result = { ["__proto__"]: { preserved: 7 }, constructor: "ordinary" }; + const evidence = createEvidence(TARGET, PROVIDER, { + operation: "health", + parameters: {}, + result, + rawResult: result, + }); + const parsed = parseEvidence(evidence); + expect(parsed.normalized_result).toEqual(result); + expect(parsed.raw_result).toEqual(result); + expect(Object.getPrototypeOf(parsed.normalized_result)).toBe( + Object.prototype, + ); + expect(Reflect.get(Object.prototype, "preserved")).toBeUndefined(); + const stripped = createEvidence(TARGET, PROVIDER, { + operation: "health", + parameters: {}, + result: { constructor: "ordinary" }, + rawResult: { constructor: "ordinary" }, + }); + expect(stripped.evidence_id).not.toBe(evidence.evidence_id); +}); diff --git a/src/domain/evidence.ts b/src/domain/evidence.ts index 014acb7ed7..a634d0c3b1 100644 --- a/src/domain/evidence.ts +++ b/src/domain/evidence.ts @@ -7,7 +7,11 @@ import { type AnalysisProfileCommitment, } from "./analysisProfile.js"; import type { BinaryTarget } from "./binaryTarget.js"; -import { jsonValueSchema, type JsonValue } from "./jsonValue.js"; +import { + jsonObjectSchema, + jsonValueSchema, + type JsonValue, +} from "./jsonValue.js"; import { digestSchema } from "./../domain/digests.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; @@ -89,7 +93,7 @@ const evidenceBaseSchema = z provider: providerSchema, predicate_type: z.string().min(1), operation: z.string().min(1), - parameters: z.record(z.string(), jsonValueSchema), + parameters: jsonObjectSchema, raw_result: jsonValueSchema.nullable(), normalized_result: jsonValueSchema, confidence: z.enum(["observed", "derived", "inferred"]), @@ -169,6 +173,8 @@ export interface EvidenceObservation { readonly authority?: EvidenceAuthority; readonly environment?: ExecutionEnvironment | null; readonly limitations?: readonly string[]; + /** Reason the subject identity is unavailable, when no target identity is supplied. */ + readonly subjectUnavailableReason?: string; readonly locations?: readonly EvidenceLocation[]; readonly evidenceLinks?: readonly string[]; } @@ -229,7 +235,7 @@ export const createEvidence = ( target === undefined ? null : { - name: target.path.split("/").at(-1) ?? target.path, + name: target.path.split("/").at(-1) || "artifact", digest: { sha256: target.sha256 }, format: target.format, architecture: target.architecture ?? null, @@ -262,20 +268,33 @@ export const createEvidence = ( environment: observation.environment ?? null, limitations: [ ...(target === undefined - ? ["Artifact identity is unavailable for this observation."] + ? [ + observation.subjectUnavailableReason ?? + "Artifact identity is unavailable for this observation.", + ] : []), ...(observation.limitations ?? []), ], locations: [...(observation.locations ?? [])], evidence_links: [...(observation.evidenceLinks ?? [])], } satisfies JsonValue; - const normalized = evidenceRecordSchema.parse({ + // Select the known envelope before parsing. A union otherwise traverses and + // clones a legacy result for the profiled branch before rejecting its absent + // profile and trying the legacy branch. + const schema = + observation.analysisProfile === undefined + ? evidenceBaseSchema + : profiledEvidenceSchema; + const normalized = schema.parse({ ...semantic, evidence_id: `ev_${"0".repeat(64)}`, subject, }); - return parseEvidence({ + // The envelope has already been parsed into an independent snapshot. Only + // its derived identifier changes here; parsing again clones the full payload + // and recomputes the same digest while the previous snapshot is still live. + return { ...normalized, evidence_id: computeEvidenceId(normalized), - }); + }; }; diff --git a/src/domain/evidenceBundle.test.ts b/src/domain/evidenceBundle.test.ts new file mode 100644 index 0000000000..00e5f7b4fc --- /dev/null +++ b/src/domain/evidenceBundle.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "vitest"; + +import { createEvidence } from "./evidence.js"; +import { + createEvidenceBundle, + evidenceBundleForTarget, + parseEvidenceBundle, +} from "./evidenceBundle.js"; +import { + createResidualUnknown, + recordUnknownInputSchema, +} from "./residualUnknown.js"; + +const provider = { id: "fixture", name: "Fixture", version: "1" }; +const targetDigest = "a".repeat(64); +const foreignDigest = "b".repeat(64); + +const makeUnknown = ( + question: string, + scopeDigest: string, + dependency?: string, +) => { + const input = recordUnknownInputSchema.parse({ + question, + severity: "medium", + domain: "bundle-projection", + required_authority: null, + required_confidence: "derived", + required_environment: null, + recommended_probes: [], + relationships: + dependency === undefined + ? [] + : [{ type: "depends-on", unknown_id: dependency }], + }); + const mutation = createEvidence(undefined, provider, { + predicateType: "rea.residual-unknown-mutation", + operation: "record_unknown", + parameters: { domain: input.domain, severity: input.severity }, + result: { + action: "record", + question: input.question, + required_authority: input.required_authority, + required_confidence: input.required_confidence, + }, + }); + return { + evidence: mutation, + unknown: createResidualUnknown(input, mutation.evidence_id, scopeDigest), + }; +}; + +describe("evidenceBundleForTarget", () => { + it("removes dependent unknowns when their foreign-scope root is excluded", () => { + const foreignRoot = makeUnknown("Foreign root", foreignDigest); + const child = makeUnknown( + "Target child", + targetDigest, + foreignRoot.unknown.unknown_id, + ); + const grandchild = makeUnknown( + "Target grandchild", + targetDigest, + child.unknown.unknown_id, + ); + const retained = makeUnknown("Independent target", targetDigest); + const bundle = createEvidenceBundle( + [ + foreignRoot.evidence, + child.evidence, + grandchild.evidence, + retained.evidence, + ], + [ + foreignRoot.unknown, + child.unknown, + grandchild.unknown, + retained.unknown, + ], + ); + + expect(evidenceBundleForTarget(bundle, targetDigest)).toEqual( + createEvidenceBundle([retained.evidence], [retained.unknown]), + ); + }); + + it("projects a valid graph with more than 125,000 dependents", () => { + const foreignRoot = makeUnknown("Foreign high-fan-out root", foreignDigest); + const middle = makeUnknown( + "Target middle depending on foreign root", + targetDigest, + foreignRoot.unknown.unknown_id, + ); + const unknowns = [foreignRoot.unknown, middle.unknown]; + for (let index = 0; index < 125_000; index += 1) { + const input = recordUnknownInputSchema.parse({ + question: `Target child ${index}`, + severity: "medium", + domain: "bundle-projection", + required_authority: null, + required_confidence: "derived", + required_environment: null, + recommended_probes: [], + relationships: [ + { type: "depends-on", unknown_id: middle.unknown.unknown_id }, + ], + }); + unknowns.push( + createResidualUnknown(input, middle.evidence.evidence_id, targetDigest), + ); + } + const bundle = parseEvidenceBundle( + createEvidenceBundle([foreignRoot.evidence, middle.evidence], unknowns), + ); + + expect(evidenceBundleForTarget(bundle, targetDigest)).toEqual( + createEvidenceBundle([], []), + ); + }); +}); diff --git a/src/domain/evidenceBundle.ts b/src/domain/evidenceBundle.ts index 665ccad7b5..807781b667 100644 --- a/src/domain/evidenceBundle.ts +++ b/src/domain/evidenceBundle.ts @@ -111,28 +111,57 @@ export const evidenceBundleForTarget = ( const recordsById = new Map( bundle.records.map((record) => [record.evidence_id, record]), ); - let unknowns = bundle.unknowns.filter( + const candidates = bundle.unknowns.filter( ({ scope_digest: scopeDigest }) => scopeDigest === sha256, ); - let changed = true; - while (changed) { - const ids = new Set(unknowns.map(({ unknown_id: id }) => id)); - const retained = unknowns.filter( - (unknown) => - unknown.relationships.every(({ unknown_id: id }) => ids.has(id)) && - referencedEvidenceIds(unknown).every((id) => { - const record = recordsById.get(id); - return ( - record?.subject?.digest.sha256 === sha256 || - (unknown.mutation_evidence_ids.includes(id) && - record?.subject === null && - record.predicate_type === "rea.residual-unknown-mutation") - ); - }), + const retained = new Set(); + const revisionsById = new Map(); + const dependents = new Map>(); + for (const unknown of candidates) { + const mutationEvidenceIds = new Set(unknown.mutation_evidence_ids); + const evidenceAvailable = referencedEvidenceIds(unknown).every((id) => { + const record = recordsById.get(id); + return ( + record?.subject?.digest.sha256 === sha256 || + (mutationEvidenceIds.has(id) && + record?.subject === null && + record.predicate_type === "rea.residual-unknown-mutation") + ); + }); + if (!evidenceAvailable) continue; + retained.add(unknown); + revisionsById.set( + unknown.unknown_id, + (revisionsById.get(unknown.unknown_id) ?? 0) + 1, ); - changed = retained.length !== unknowns.length; - unknowns = retained; + for (const relatedId of new Set( + unknown.relationships.map(({ unknown_id: id }) => id), + )) { + const references = + dependents.get(relatedId) ?? new Set(); + references.add(unknown); + dependents.set(relatedId, references); + } } + const pending: ResidualUnknown[] = []; + for (const unknown of retained) + if ( + unknown.relationships.some(({ unknown_id: id }) => !revisionsById.has(id)) + ) + pending.push(unknown); + for (let index = 0; index < pending.length; index += 1) { + const unknown = pending[index]; + if (unknown === undefined || !retained.delete(unknown)) continue; + const remaining = (revisionsById.get(unknown.unknown_id) ?? 1) - 1; + if (remaining > 0) { + revisionsById.set(unknown.unknown_id, remaining); + continue; + } + revisionsById.delete(unknown.unknown_id); + for (const dependent of dependents.get(unknown.unknown_id) ?? []) + pending.push(dependent); + } + const unknowns = candidates.filter((unknown) => retained.has(unknown)); const mutationIds = new Set( unknowns.flatMap(({ mutation_evidence_ids: ids }) => ids), ); @@ -153,6 +182,34 @@ const referencedEvidenceIds = (unknown: ResidualUnknown): readonly string[] => [ ...(unknown.resolution?.evidence_ids ?? []), ]; +/** + * Name the bundle constraint that failed and how to recover. A single + * Evidence record, such as one command result, is not a bundle. + */ +export const describeEvidenceBundleFailure = ( + input: unknown, + cause: unknown, +): string => { + const record = evidenceRecordSchema.safeParse(input); + if (record.success) + return `Expected an Evidence bundle with records and manifest arrays, but this JSON is one Evidence record (${record.data.evidence_id}). Export a session bundle with the MCP export_evidence_bundle tool and supply that file.`; + return cause instanceof z.ZodError + ? `Evidence bundle does not match the bundle schema ${describeValidationFailure(cause)}. Supply an unmodified bundle written by export_evidence_bundle or rea evidence-export.` + : `Evidence bundle validation failed: ${describeValidationFailure(cause)}. Recreate or re-export the bundle, then try again.`; +}; + +/** Locate the first schema issue, or keep a thrown constraint message. */ +export const describeValidationFailure = (cause: unknown): string => { + if (!(cause instanceof z.ZodError)) + return cause instanceof Error ? cause.message : String(cause); + const [first, ...rest] = cause.issues; + const location = + first === undefined || first.path.length === 0 + ? "the document root" + : first.path.map(String).join("."); + return `at ${location}: ${first?.message ?? "invalid value"}${rest.length === 0 ? "" : ` (${String(rest.length)} more issues)`}`; +}; + /** Parse records, verify semantic IDs, and reject inconsistent manifests. */ export const parseEvidenceBundle = (input: unknown): EvidenceBundle => { const parsed = evidenceBundleSchema.parse(input); @@ -285,26 +342,28 @@ export const validateResidualUnknownAddition = ( ); if (unknown.resolution?.disposition === "verified") validateVerifiedResolution(unknown, evidenceById); - for (const relationship of unknown.relationships) + // In a valid graph, no existing node can point to an absent ID. Since the + // schema rejects self-relations, a new node's outgoing edges cannot close a + // cycle; keep graph traversal for revisions of existing nodes only. + if (current !== undefined) { + const dependencies = unknown.relationships + .filter((relationship) => relationship.type === "depends-on") + .map(({ unknown_id }) => unknown_id); if ( - relationship.type === "depends-on" && - reachesUnknown( - relationship.unknown_id, - unknown.unknown_id, - currentHeads, - unknown, - ) + dependencies.length > 0 && + reachesUnknown(dependencies, unknown.unknown_id, currentHeads, unknown) ) throw new TypeError("Residual unknown dependency graph contains a cycle"); + } }; const reachesUnknown = ( - start: string, + starts: readonly string[], goal: string, currentHeads: ReadonlyMap, candidate: ResidualUnknown, ): boolean => { - const pending = [start]; + const pending = [...starts]; const visited = new Set(); while (pending.length > 0) { const id = pending.pop(); @@ -356,20 +415,56 @@ const rejectDependencyCycles = ( ): void => { const visited = new Set(); const active = new Set(); - const visit = (id: string): void => { - if (active.has(id)) - throw new TypeError("Residual unknown dependency graph contains a cycle"); - if (visited.has(id)) return; - active.add(id); - const unknown = unknownById.get(id); - if (unknown === undefined) + const pending: { + readonly id: string; + readonly relationships: ResidualUnknown["relationships"]; + nextRelationship: number; + }[] = []; + for (const root of unknownById.keys()) { + if (visited.has(root)) continue; + const rootUnknown = unknownById.get(root); + if (rootUnknown === undefined) throw new TypeError("Residual unknown dependency graph is inconsistent"); - for (const relationship of unknown.relationships) - if (relationship.type === "depends-on") visit(relationship.unknown_id); - active.delete(id); - visited.add(id); - }; - for (const id of unknownById.keys()) visit(id); + active.add(root); + pending.push({ + id: root, + relationships: rootUnknown.relationships, + nextRelationship: 0, + }); + while (pending.length > 0) { + const frame = pending[pending.length - 1]; + if (frame === undefined) break; + let descended = false; + while (frame.nextRelationship < frame.relationships.length) { + const relationship = frame.relationships[frame.nextRelationship]; + frame.nextRelationship += 1; + if (relationship?.type !== "depends-on") continue; + const dependency = relationship.unknown_id; + if (active.has(dependency)) + throw new TypeError( + "Residual unknown dependency graph contains a cycle", + ); + if (visited.has(dependency)) continue; + const unknown = unknownById.get(dependency); + if (unknown === undefined) + throw new TypeError( + "Residual unknown dependency graph is inconsistent", + ); + active.add(dependency); + pending.push({ + id: dependency, + relationships: unknown.relationships, + nextRelationship: 0, + }); + descended = true; + break; + } + if (descended) continue; + pending.pop(); + active.delete(frame.id); + visited.add(frame.id); + } + } }; /** Encode a validated bundle as byte-stable RFC 8785 canonical JSON. */ diff --git a/src/domain/evidenceErrors.ts b/src/domain/evidenceErrors.ts index 2e84cd83d4..f6e0c3237f 100644 --- a/src/domain/evidenceErrors.ts +++ b/src/domain/evidenceErrors.ts @@ -1,8 +1,20 @@ -import { AnalysisError } from "./analysisErrorBase.js"; +import { + AnalysisError, + type AnalysisErrorOptions, +} from "./analysisErrorBase.js"; /** Evidence identity, schema, or bundle manifests failed integrity checks. */ export class EvidenceIntegrityError extends AnalysisError { readonly _tag = "EvidenceIntegrityError"; + override readonly userMessage: string | undefined; + + constructor( + message: string, + options?: AnalysisErrorOptions & { readonly userMessage?: string }, + ) { + super(message, options); + this.userMessage = options?.userMessage; + } } /** A session Evidence reference is missing or has the wrong semantic identity. */ @@ -25,11 +37,15 @@ export class EvidenceReferenceError extends EvidenceIntegrityError { export class EvidenceFileError extends AnalysisError { readonly _tag = "EvidenceFileError"; + /** Caller-selected path that failed, resolved against the working directory. */ + readonly path: string | undefined; + constructor( readonly operation: "read" | "write", - readonly reason: "not-file" | "exists" | "invalid-json" | "io", - options?: ErrorOptions, + readonly reason: "not-file" | "exists" | "invalid-json" | "missing" | "io", + options?: ErrorOptions & { readonly path?: string }, ) { super(`Evidence bundle ${operation} failed: ${reason}`, options); + this.path = options?.path; } } diff --git a/src/domain/evm/evmInterface.test.ts b/src/domain/evm/evmInterface.test.ts new file mode 100644 index 0000000000..659f838ba5 --- /dev/null +++ b/src/domain/evm/evmInterface.test.ts @@ -0,0 +1,36 @@ +import { expect, it } from "vitest"; +import { evmInterfaceSchema } from "./evmInterface.js"; +import { evmInterfaceFixture } from "../../../tests/fixtures/evm/interface.js"; + +it("preserves original carrier and decoded byte identity as distinct observations", () => { + const fixture = evmInterfaceFixture(); + expect(evmInterfaceSchema.parse(fixture)).toEqual(fixture); + expect(fixture.artifact.sha256).not.toBe(fixture.bytecode.sha256); +}); +it.each(["byte-count", "body-offset", "selector", "authenticity"])( + "rejects false interface evidence: %s", + (problem) => { + const value = evmInterfaceFixture(); + const raw = { + ...value, + ...(problem === "authenticity" + ? { + bytecode: { + ...value.bytecode, + deployment_authenticity: "verified", + }, + } + : {}), + }; + if (problem === "byte-count") raw.bytecode.bytes++; + if (problem === "body-offset" || problem === "selector") + raw.functions.push({ + selector: problem === "selector" ? "bad" : "0xaabbccdd", + bytecode_offset: problem === "body-offset" ? 5 : 0, + dispatch: "abi", + inferred_arguments: null, + inferred_state_mutability: null, + }); + expect(evmInterfaceSchema.safeParse(raw).success).toBe(false); + }, +); diff --git a/src/domain/evm/evmInterface.ts b/src/domain/evm/evmInterface.ts new file mode 100644 index 0000000000..75ada1f971 --- /dev/null +++ b/src/domain/evm/evmInterface.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +const byteOffset = z.number().int().nonnegative(); +const sha256 = z.string().regex(/^[a-f0-9]{64}$/); + +/** Select the carrier representation explicitly; no RPC or deployed-contract lookup is implied. */ +export const inspectEvmInterfaceInputSchema = z.strictObject({ + path: z + .string() + .min(1) + .describe("Absolute path to caller-selected local bytecode carrier"), + encoding: z + .enum(["raw", "hex"]) + .describe( + "Raw bytes or UTF-8 hex with optional 0x prefix and outer ASCII whitespace", + ), +}); + +/** Recovered interface candidates are inferences, independently of observed byte identity. */ +export const evmInterfaceSchema = z + .strictObject({ + artifact: z.strictObject({ + path: z.string().min(1), + sha256, + bytes: byteOffset, + encoding: z.enum(["raw", "hex"]), + }), + bytecode: z.strictObject({ + sha256, + bytes: byteOffset, + hex: z.string().regex(/^(?:[0-9a-f]{2})*$/), + digest_algorithm: z.literal("sha256"), + kind: z.literal("unknown"), + deployment_authenticity: z.literal("unknown"), + hardfork: z.literal("unknown"), + }), + evidence_kind: z.literal("inferred"), + functions: z.array( + z.strictObject({ + selector: z.string().regex(/^0x[0-9a-f]{8}$/), + bytecode_offset: byteOffset, + dispatch: z.enum(["abi", "fallback"]), + inferred_arguments: z.string().nullable(), + inferred_state_mutability: z.string().nullable(), + }), + ), + discovery_completeness: z.literal("unknown"), + runtime_execution: z.literal("not-performed"), + diagnostics: z.strictObject({ + stdout: z.string(), + stderr: z.string(), + truncated: z.boolean(), + }), + limitations: z.array(z.string()), + }) + .superRefine((value, context) => { + if (value.bytecode.hex.length !== value.bytecode.bytes * 2) + context.addIssue({ + code: "custom", + path: ["bytecode"], + message: + "Selected byte count does not match the bytecode hex representation.", + }); + for (const [index, fn] of value.functions.entries()) + if (fn.bytecode_offset >= value.bytecode.bytes) + context.addIssue({ + code: "custom", + path: ["functions", index, "bytecode_offset"], + message: "Inferred body offset lies outside the selected bytecode.", + }); + }); + +export type InspectEvmInterfaceInput = z.infer< + typeof inspectEvmInterfaceInputSchema +>; +export type EvmInterface = z.infer; diff --git a/src/domain/explicitSensitiveFailure.test.ts b/src/domain/explicitSensitiveFailure.test.ts new file mode 100644 index 0000000000..28d935b826 --- /dev/null +++ b/src/domain/explicitSensitiveFailure.test.ts @@ -0,0 +1,126 @@ +import { AnalysisAccessDeniedError } from "./analysisErrorCore.js"; +import { expect, it } from "vitest"; +import { + AnalysisInputError, + AnalysisCapabilityUnavailableError, +} from "./analysisErrorCore.js"; +import { projectAnalysisError } from "./analysisErrorProjection.js"; +import { ProviderCleanupError } from "./providerCleanupError.js"; +import { + redactExplicitFailure, + redactExplicitPointer, +} from "./explicitSensitiveFailure.js"; + +it.each([ + ["/metadata/private~1~0/numeric", ["private/~"], "/metadata"], + ["/metadata/private/child", ["private"], "/metadata"], + ["/ordinary/path", [], "/ordinary/path"], +])( + "preserves a real parent for excluded pointer identities: %s", + (pointer, values, expected) => { + expect(redactExplicitPointer(pointer, values)).toBe(expected); + }, +); + +it("preserves cleanup failure authority and resources while excluding diagnostics", () => { + const error = new ProviderCleanupError( + "har", + ["/root/private/capture", "pid:123"], + { + capture_path: "/root/private/capture", + nested: { "private-key": 5, ordinary: "private ordinary" }, + exit_code: 2, + }, + { operation: "inspect_web_network_capture" }, + ); + const projected = projectAnalysisError( + redactExplicitFailure(error, ["private", "REDACTED"]), + ); + expect(projected.code).toBe("cleanup_incomplete"); + expect(JSON.stringify(projected)).not.toContain("private"); + expect(JSON.stringify(projected)).not.toContain("REDACTED"); + expect(JSON.stringify(projected)).toContain("pid:123"); + expect(JSON.stringify(projected)).toContain('"exit_code":2'); +}); + +it("keeps unsupported profile reasons distinct from malformed input", () => { + const error = new AnalysisCapabilityUnavailableError( + "mitmproxy", + "inspect_web_network_capture", + "Unsupported private profile", + { userMessage: "Select private-compatible profile" }, + ); + const result = redactExplicitFailure(error, ["private"]); + expect(result).toBeInstanceOf(AnalysisCapabilityUnavailableError); + expect(projectAnalysisError(result).code).toBe("capability_unavailable"); + expect(JSON.stringify(projectAnalysisError(result))).not.toContain("private"); +}); + +it("coarsens format issue pointers without creating replacement coordinates", () => { + const error = new AnalysisInputError( + "inspect_web_network_capture", + undefined, + [ + { + path: ["capture_path", "/metadata/private~1~0/nested"], + reason: "invalid_format", + message: "Malformed private field", + }, + ], + ); + const result = redactExplicitFailure(error, ["private/~", "private"]); + expect(result).toBeInstanceOf(AnalysisInputError); + if (!(result instanceof AnalysisInputError)) + throw new Error("Input error required"); + expect(result.issues[0]?.path).toEqual(["capture_path", "/metadata"]); +}); + +it("excludes a marked permission-denied path without losing its host error code", () => { + const error = redactExplicitFailure( + new AnalysisAccessDeniedError( + "inspect_web_network_capture", + "/declared-path-value/capture.har", + "EACCES", + ), + ["declared-path-value"], + ); + expect(error._tag).toBe("AnalysisAccessDeniedError"); + const projected = projectAnalysisError(error); + expect(projected).toMatchObject({ + code: "access_denied", + category: "unavailable", + details: { system_code: "EACCES" }, + }); + expect(JSON.stringify(projected)).not.toContain("declared-path-value"); +}); + +it("coarsens ordinary sensitive issue-path segments to their real parent", () => { + const input = new AnalysisInputError( + "inspect_web_network_capture", + undefined, + [ + { + path: ["input", "mysecret", "child"], + reason: "unknown_argument", + message: "Unrecognized mysecret", + }, + ], + ); + const result = redactExplicitFailure(input, ["secret"]); + if (!(result instanceof AnalysisInputError)) + throw new Error("Input error required"); + expect(result.issues[0]?.path).toEqual(["input"]); + expect(JSON.stringify(projectAnalysisError(result))).not.toContain("secret"); +}); + +it("does not reinterpret an unknown argument name as a JSON pointer", () => { + const input = new AnalysisInputError( + "inspect_web_network_capture", + undefined, + [{ path: ["/ordinary/secret"], reason: "unknown_argument" }], + ); + const result = redactExplicitFailure(input, ["secret"]); + if (!(result instanceof AnalysisInputError)) + throw new Error("Input error required"); + expect(result.issues[0]?.path).toEqual([]); +}); diff --git a/src/domain/explicitSensitiveFailure.ts b/src/domain/explicitSensitiveFailure.ts new file mode 100644 index 0000000000..f26324f9b6 --- /dev/null +++ b/src/domain/explicitSensitiveFailure.ts @@ -0,0 +1,136 @@ +import type { AnalysisError } from "./analysisErrorBase.js"; +import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisInputError, + AnalysisOutputError, + AnalysisCapabilityUnavailableError, +} from "./analysisErrorCore.js"; +import { redactExplicitText } from "./explicitSensitiveValues.js"; +import type { JsonValue } from "./jsonValue.js"; +import { ProviderAdapterError } from "./providerAdapterError.js"; +import { ProviderCleanupError } from "./providerCleanupError.js"; + +/** Keep a real JSON-pointer ancestor when a property identity is explicitly excluded. */ +export const redactExplicitPointer = ( + pointer: string, + values: readonly string[], +): string => { + let parent = ""; + for (const segment of pointer.split("/").slice(1)) { + const decoded = segment.replaceAll("~1", "/").replaceAll("~0", "~"); + const child = `${parent}/${segment}`; + if ( + values.some( + (literal) => decoded.includes(literal) || child.includes(literal), + ) + ) + return parent; + parent = child; + } + return parent; +}; + +/** Exclude explicit literals from common acquisition/decoder failures, preserving typed diagnostics. */ +export const redactExplicitFailure = ( + error: AnalysisError, + values: readonly string[], +): AnalysisError => { + if (values.length === 0) return error; + const text = (value: string): string => redactExplicitText(value, values); + const path = ( + parts: readonly (string | number)[], + ordinaryNames: boolean, + ): (string | number)[] => { + const retained: (string | number)[] = []; + for (const part of parts) { + if (typeof part !== "string") { + retained.push(part); + } else if (!ordinaryNames && part.startsWith("/")) { + const pointer = redactExplicitPointer(part, values); + retained.push(pointer); + if (pointer !== part) break; + } else { + if (values.some((literal) => part.includes(literal))) break; + retained.push(part); + } + } + return retained; + }; + const diagnostics = ( + value: Readonly>, + ): Record => + Object.fromEntries( + Object.entries(value) + .filter(([key]) => !values.some((literal) => key.includes(literal))) + .map(([key, item]) => [key, visit(item)]), + ); + const visit = (value: JsonValue): JsonValue => { + if (typeof value === "string") return text(value); + if (Array.isArray(value)) return value.map(visit); + if (typeof value === "object" && value !== null) return diagnostics(value); + return value; + }; + if (error instanceof AnalysisAccessDeniedError) + return new AnalysisAccessDeniedError( + error.operation, + text(error.path), + error.systemCode, + { cause: error }, + ); + if (error instanceof AnalysisArtifactChangedError) + return new AnalysisArtifactChangedError( + error.operation, + text(error.path), + text(error.reason), + { cause: error }, + ); + if (error instanceof AnalysisInputError) + return new AnalysisInputError( + error.operation, + { cause: error }, + error.issues.map((issue) => ({ + ...issue, + // Unknown-argument paths contain literal field names, including names + // beginning with '/'; they are not producer JSON-pointer coordinates. + path: path(issue.path, issue.reason === "unknown_argument"), + ...(issue.message === undefined + ? {} + : { message: text(issue.message) }), + ...(issue.expected === undefined + ? {} + : { expected: visit(issue.expected) }), + })), + ); + if (error instanceof AnalysisOutputError) + return new AnalysisOutputError(error.operation, text(error.reason), { + cause: error, + }); + if (error instanceof AnalysisCapabilityUnavailableError) + return new AnalysisCapabilityUnavailableError( + error.providerId, + error.operation, + text(error.reason), + { + cause: error, + ...(error.userMessage === undefined + ? {} + : { userMessage: text(error.userMessage) }), + }, + ); + if (error instanceof ProviderCleanupError) + return new ProviderCleanupError( + error.providerId, + error.cleanupResources.map(text), + diagnostics(error.diagnostics ?? {}), + { operation: error.operation, cause: error }, + ); + if (error instanceof ProviderAdapterError) + return new ProviderAdapterError(error.providerId, error.operation, { + cause: error, + ...(error.diagnostics === undefined + ? {} + : { diagnostics: diagnostics(error.diagnostics) }), + }); + return error; +}; diff --git a/src/domain/explicitSensitiveValues.test.ts b/src/domain/explicitSensitiveValues.test.ts new file mode 100644 index 0000000000..64c03a9d72 --- /dev/null +++ b/src/domain/explicitSensitiveValues.test.ts @@ -0,0 +1,22 @@ +import { expect, it } from "vitest"; +import { redactExplicitText } from "./explicitSensitiveValues.js"; + +it.each([ + ["secret context", ["secret"]], + ["secret context", ["secret", "REDACTED"]], + ["secret context", ["secret", "[", "…"]], + ["secretable context", ["secret", "able", "table"]], + ["secretsecret context", ["secret", "…", "REDACTED"]], +] as const)( + "keeps declared literals out of replacement text: %s", + (input, values) => { + const result = redactExplicitText(input, values); + for (const value of values) expect(result).not.toContain(value); + }, +); + +it("does not guess sensitivity or treat empty declarations as matches", () => { + expect(redactExplicitText("API_SECRET ordinary evidence", [""])).toBe( + "API_SECRET ordinary evidence", + ); +}); diff --git a/src/domain/explicitSensitiveValues.ts b/src/domain/explicitSensitiveValues.ts new file mode 100644 index 0000000000..52c115c81a --- /dev/null +++ b/src/domain/explicitSensitiveValues.ts @@ -0,0 +1,20 @@ +/** Replace caller-declared literal text values without guessing sensitivity from names. */ +export const redactExplicitText = ( + value: string, + sensitiveValues: readonly string[], +): string => { + const literals = [...new Set(sensitiveValues)].filter( + (literal) => literal !== "", + ); + const replacement = + ["[REDACTED]", "…", ""].find((candidate) => + literals.every((literal) => !candidate.includes(literal)), + ) ?? ""; + let result = value; + for (const literal of literals.sort( + (left, right) => right.length - left.length, + )) + result = result.replaceAll(literal, replacement); + // Replacement boundaries can form another declaration; exclude that text. + return literals.some((literal) => result.includes(literal)) ? "" : result; +}; diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index 31bb12c3f1..8a7f1f5de4 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -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. */ diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts index 706dc77597..4adf6becde 100644 --- a/src/domain/functionComparison.test.ts +++ b/src/domain/functionComparison.test.ts @@ -246,6 +246,112 @@ describe("function collection collation ties", () => { }); }); +describe("function comparison ordering controls", () => { + const composed = { address: "0x2000", name: "caf\u00e9" }; + const decomposed = { address: "0x3000", name: "cafe\u0301" }; + + it("preserves duplicate Unicode collections and nullable endpoints", () => { + const controlled = (base: "0x1000" | "0x2000", reverse: boolean) => { + const endpoint = base === "0x1000" ? "0x1010" : "0x2010"; + const edges = [ + { + source_address: base, + target_address: endpoint, + source_procedure: null, + target_procedure: { address: endpoint, name: "caf\u00e9" }, + kind: { + available: true, + provenance: "provider-reference-manager", + type: "UNCONDITIONAL_CALL", + flow: true, + call: true, + jump: false, + data: false, + read: false, + write: false, + indirect: false, + computed: false, + conditional: false, + terminal: false, + primary: true, + operand_index: 0, + external: false, + }, + }, + { + source_address: base, + target_address: endpoint, + source_procedure: { address: base, name: "dispatch" }, + target_procedure: null, + kind: { + available: true, + provenance: "provider-reference-manager", + type: "READ", + flow: false, + call: false, + jump: false, + data: true, + read: true, + write: false, + indirect: false, + computed: false, + conditional: false, + terminal: false, + primary: true, + operand_index: 1, + external: false, + }, + }, + ]; + const strings = [ + { address: endpoint, source_address: base, value: "caf\u00e9" }, + { address: endpoint, source_address: base, value: "cafe\u0301" }, + { address: endpoint, source_address: base, value: "cafe\u0301" }, + ]; + return functionDossierSchema.parse({ + ...dossier("return 0;", base), + callees: [composed, decomposed, composed], + incoming_references: reverse ? [...edges].reverse() : edges, + outgoing_references: reverse ? [...edges].reverse() : edges, + referenced_strings: reverse ? [...strings].reverse() : strings, + referenced_names: reverse ? [...strings].reverse() : strings, + }); + }; + const makeEvidence = (digit: string, result: unknown): Evidence => + createEvidence( + { + path: `/tmp/function-${digit}`, + sha256: digit.repeat(64), + format: "mach-o", + }, + { id: "rea-workflow", name: "REA workflow", version: "1" }, + { + operation: "analyze_function", + parameters: enhancedInputSchemas.analyze_function.parse({ + procedure: "dispatch", + }), + result: jsonValueSchema.parse(result), + confidence: "derived", + authority: "shipped-artifact", + }, + ); + const left = controlled("0x1000", false); + const right = controlled("0x2000", true); + const { native_api: _nativeApi, ...rightWithoutNativeApi } = right; + + const comparison = compareFunctions( + makeEvidence("d", left), + makeEvidence("e", rightWithoutNativeApi), + ); + + expect(comparison.status).toBe("unchanged"); + for (const dimension of ["calls", "references", "strings_names"] as const) + expect( + comparison.dimensions.find((item) => item.dimension === dimension), + ).toMatchObject({ status: "unchanged" }); + }); +}); + describe("function comparison CFG address normalization", () => { it("matches CFG successors by numeric address", () => { const make = (base: "0x1000" | "0x2000") => diff --git a/src/domain/functionComparisonNormalization.ts b/src/domain/functionComparisonNormalization.ts index 43871b123a..804127675c 100644 --- a/src/domain/functionComparisonNormalization.ts +++ b/src/domain/functionComparisonNormalization.ts @@ -177,15 +177,19 @@ export const stringAndNameProjection = ( }; export const sorted = (values: readonly unknown[]): readonly unknown[] => - [...values].sort((left, right) => { - const leftJson = canonicalJson(left, "Function normalization"); - const rightJson = canonicalJson(right, "Function normalization"); - // Preserve existing collation while ordering distinct values within a tie. - return ( - leftJson.localeCompare(rightJson) || - compareCodePoints(leftJson, rightJson) - ); - }); + values + .map((value) => ({ + value, + json: canonicalJson(value, "Function normalization"), + })) + .sort((left, right) => { + // Preserve existing collation while ordering distinct values within a tie. + return ( + left.json.localeCompare(right.json) || + compareCodePoints(left.json, right.json) + ); + }) + .map(({ value }) => value); export const combineCoverage = ( left: FunctionCollection, diff --git a/src/domain/inputIssueProjection.test.ts b/src/domain/inputIssueProjection.test.ts index 0abc834319..45a241d9ee 100644 --- a/src/domain/inputIssueProjection.test.ts +++ b/src/domain/inputIssueProjection.test.ts @@ -2,8 +2,45 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; import { projectInputIssues } from "./inputIssueProjection.js"; +import { processScenarioSchema } from "./process/processScenario.js"; describe("input issue projection", () => { + it("preserves static regex guidance without echoing the rejected value", () => { + const schema = z.object({ + value: z.string().regex(/^[^\0]*$/u, "Values cannot contain NUL"), + }); + const input = { value: "private\0value" }; + const parsed = schema.safeParse(input); + if (parsed.success) throw new Error("expected invalid input"); + + expect(projectInputIssues(parsed.error.issues, input)).toEqual([ + { + path: ["value"], + reason: "invalid_format", + expected: "regex", + message: "Values cannot contain NUL", + }, + ]); + }); + + it("projects reserved process environment key constraints at the key path", () => { + const input = { + executable: "/usr/bin/true", + environment: { REA_PROCESS_RUN_ID: "caller-value" }, + }; + const parsed = processScenarioSchema.safeParse(input); + if (parsed.success) throw new Error("expected invalid input"); + + expect(projectInputIssues(parsed.error.issues, input)).toEqual([ + { + path: ["environment", "REA_PROCESS_RUN_ID"], + reason: "invalid_format", + expected: "regex", + message: "REA_PROCESS_RUN_ID is reserved by the process adapter", + }, + ]); + }); + it("preserves schema-authored custom correction guidance", () => { const schema = z .object({ left: z.string().optional(), right: z.string().optional() }) diff --git a/src/domain/inputIssueProjection.ts b/src/domain/inputIssueProjection.ts index 08390df5a9..31d158743d 100644 --- a/src/domain/inputIssueProjection.ts +++ b/src/domain/inputIssueProjection.ts @@ -27,6 +27,13 @@ const projectIssue = ( ), ), ); + if (issue.code === "invalid_key") + return issue.issues.flatMap((keyIssue) => + projectIssue( + { ...keyIssue, path: [...issue.path, ...keyIssue.path] }, + input, + ), + ); if (issue.code === "unrecognized_keys") return issue.keys.map((key) => ({ path: [...path, key], @@ -48,7 +55,16 @@ const projectIssue = ( if (issue.code === "too_big") return [boundedIssue(path, "maximum", numericBound(issue.maximum))]; if (issue.code === "invalid_format") - return [{ path, reason: "invalid_format", expected: issue.format }]; + return [ + { + path, + reason: "invalid_format", + expected: issue.format, + ...(issue.format === "regex" && issue.message.length > 0 + ? { message: issue.message } + : {}), + }, + ]; if (issue.code === "invalid_value") return [ { diff --git a/src/domain/javascript/electronActiveObservation.ts b/src/domain/javascript/electronActiveObservation.ts index 02d60b7481..6c1391266b 100644 --- a/src/domain/javascript/electronActiveObservation.ts +++ b/src/domain/javascript/electronActiveObservation.ts @@ -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([]), }); diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index 16569d46f2..024df75824 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -2,156 +2,46 @@ import { describe, expect, it } from "vitest"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; -describe.each(["webPreferences", "serviceName"] as const)( - "Electron %s option lookup", - (name) => { - const value = - name === "webPreferences" - ? '{ preload: "./kept.js", sandbox: true }' - : '"kept"'; - const explicit = `${name}: ${value}`; - const method = `${name}() { return ${value}; }`; - const getter = `get ${name}() { return ${value}; }`; - const setter = `set ${name}(value) {}`; +describe("Electron option evidence", () => { + it("keeps the final explicit preload and service name with unrelated keys", () => { + const analysis = analyzeJavaScriptStaticSource(` + new BrowserWindow({ webPreferences: { preload: "./kept.js", sandbox: true }, title: "App" }); + utilityProcess.fork("./worker.js", [], { serviceName: "kept", cwd: "/app" }); + `); - const explicitCases = [ - ["plain key", explicit], - ["quoted key", `"${name}": ${value}`], - ["computed string key", `["${name}"]: ${value}`], - ["duplicate key", `${name}: null, ${explicit}`], - ["computed duplicate key", `${name}: null, ["${name}"]: ${value}`], - ["earlier spread", `...options, ${explicit}`], - ["earlier computed identifier", `[key]: null, ${explicit}`], - ["earlier computed expression", `[getKey()]: null, ${explicit}`], - ["earlier getter", `${getter}, ${explicit}`], - ["earlier method", `${method}, ${explicit}`], - ["unrelated later key", `${explicit}, title: "title"`], - ["unrelated computed literal", `${explicit}, ["title"]: null`], - ["unrelated numeric key", `${explicit}, [1]: null`], - ["unrelated empty key", `${explicit}, [""]: null`], - ["unrelated getter", `${explicit}, get title() { return "title"; }`], - ]; - - it.each(explicitCases)( - "retains the explicit value with %s", - (_, members) => { - const analysis = analyzeOption(name, `{ ${members} }`); - expect(analysis.parse_status).toBe("complete"); - if (name === "webPreferences") { - expect(analysis.electron.browser_windows).toEqual([ - expect.objectContaining({ - options_status: "object-literal", - web_preferences_status: "object-literal", - preload_path: "./kept.js", - preload_resolution_context: "module-specifier", - web_preferences: [ - { name: "preload", value: literal("./kept.js") }, - { name: "sandbox", value: literal(true) }, - ], - }), - ]); - } else { - expect(analysis.electron.utility_processes).toEqual([ - expect.objectContaining({ service_name: "kept" }), - ]); - } - }, - ); - - const dynamicCases = [ - ["same-spelled computed binding", `[${name}]: ${value}`], - ["different computed binding", `[key]: ${value}`], - ["computed expression", `[getKey()]: ${value}`], - ["unevaluated template key", `[\`${name}\`]: ${value}`], - ["spread only", "...options"], - ["later spread", `${explicit}, ...options`], - ["later computed binding", `${explicit}, [key]: null`], - ["later computed expression", `${explicit}, [getKey()]: null`], - ["exact getter", getter], - ["exact setter", setter], - ["exact method", method], - ["computed string method", `["${name}"]() { return ${value}; }`], - ["overriding getter", `${explicit}, ${getter}`], - ["overriding setter", `${explicit}, ${setter}`], - ["overriding method", `${explicit}, ${method}`], - ["dynamic explicit value", `${name}: dynamicValue`], - ["null explicit value", `${name}: null`], - ["duplicate null value", `${explicit}, ${name}: null`], - ]; - - it.each(dynamicCases)("preserves uncertainty for %s", (_, members) => { - expectUnknownOption(name, `{ ${members} }`, "object-literal"); - }); - - it.each(["null", "options"])( - "preserves nonliteral options %s", - (options) => { - expectUnknownOption(name, options, "dynamic"); - }, - ); - - it.each(["", "{}", '{ title: "title", ["other"]: null }'])( - "reports an omitted option in %s without inventing a default", - (options) => { - const analysis = analyzeOption(name, options); - expect(analysis.parse_status).toBe("complete"); - if (name === "webPreferences") - expect(analysis.electron.browser_windows[0]).toMatchObject({ - options_status: options === "" ? "missing" : "object-literal", - web_preferences_status: "missing", - web_preferences: [], - preload_path: null, - preload_resolution_context: null, - }); - else - expect( - analysis.electron.utility_processes[0]?.service_name, - ).toBeNull(); - }, - ); - }, -); - -type OptionName = "webPreferences" | "serviceName"; - -const analyzeOption = (name: OptionName, options: string) => - analyzeJavaScriptStaticSource(` - const webPreferences = "title"; - const serviceName = "env"; - ${ - name === "webPreferences" - ? `new BrowserWindow(${options});` - : `utilityProcess.fork("./worker.js", []${options === "" ? "" : `, ${options}`});` - } - `); - -const literal = (value: string | boolean) => ({ - status: "literal", - value, - expression: null, -}); - -const expectUnknownOption = ( - name: OptionName, - options: string, - optionsStatus: "object-literal" | "dynamic", -): void => { - const analysis = analyzeOption(name, options); - expect(analysis.parse_status).toBe("partial"); - expect(analysis.parse_error_count).toBe(0); - expect(analysis.limitations.join(" ")).toContain("remain unknown"); - if (name === "webPreferences") + expect(analysis.parse_status).toBe("complete"); expect(analysis.electron.browser_windows[0]).toMatchObject({ - options_status: optionsStatus, - web_preferences_status: "dynamic", - web_preferences: [], - preload_path: null, - preload_resolution_context: null, + preload_path: "./kept.js", + preload_resolution_context: "module-specifier", + web_preferences: [ + { name: "preload", value: { status: "literal", value: "./kept.js" } }, + { name: "sandbox", value: { status: "literal", value: true } }, + ], + }); + expect(analysis.electron.utility_processes[0]).toMatchObject({ + module_path: "./worker.js", + service_name: "kept", + }); + }); + + it("keeps values unknown when a later computed key can override them", () => { + const analysis = analyzeJavaScriptStaticSource(` + new BrowserWindow({ webPreferences: { preload: "./maybe.js" }, [key]: null }); + utilityProcess.fork("./worker.js", [], { serviceName: "maybe", [key]: null }); + `); + + expect(analysis.parse_status).toBe("partial"); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.limitations.join(" ")).toContain("remain unknown"); + expect(analysis.electron.browser_windows[0]).toMatchObject({ + preload_path: null, + preload_resolution_context: null, + web_preferences_status: "dynamic", + web_preferences: [], }); - else expect(analysis.electron.utility_processes[0]).toMatchObject({ module_path: "./worker.js", - module_expression: null, service_name: null, }); -}; + }); +}); diff --git a/src/domain/javascript/htmlArtifactReferences.test.ts b/src/domain/javascript/htmlArtifactReferences.test.ts new file mode 100644 index 0000000000..d5e84d5db1 --- /dev/null +++ b/src/domain/javascript/htmlArtifactReferences.test.ts @@ -0,0 +1,70 @@ +import { expect, it } from "vitest"; + +import { htmlArtifactReferences } from "./htmlArtifactReferences.js"; + +it.each([ + ['', []], + ['', []], + [``, []], + ["", ["actual.js"]], + ['', ["actual.js"]], + ['', ["actual.js"]], + ["", ["actual.js"]], + ['', ["actual.js"]], + ['', ["actual.js"]], + ['', []], + ['', ["actual.js"]], + ['', []], + ['', []], + ['', []], + [``, []], + [ + '', + [], + ], + ['', []], + [ + '', + ["actual.js"], + ], + // The existing extraction contract does not classify script MIME types. + ['', ["actual.js"]], +])("extracts document references from %s", (text, expected) => { + expect( + htmlArtifactReferences(text).scripts.map(({ scriptPath }) => scriptPath), + ).toEqual(expected); +}); + +it("retains exact opening-tag offsets rather than the script body", () => { + const opening = ''; + const prefix = "\n\n"; + const text = `${prefix}${opening}ignored body`; + expect(htmlArtifactReferences(text)).toEqual({ + baseHref: null, + scripts: [ + { + scriptPath: "actual.js", + startOffset: prefix.length, + endOffset: prefix.length + opening.length, + }, + ], + }); +}); + +it.each([ + ['', "actual/"], + ['', "actual/"], + ['', ""], + ['', "actual/"], + ['', "actual/"], +])("selects the first real base href from %s", (text, expected) => { + expect(htmlArtifactReferences(text).baseHref).toBe(expected); +}); + +it("traverses deep valid document content without recursive stack growth", () => { + const depth = 12_000; + const text = `${"
".repeat(depth)}${"
".repeat(depth)}`; + expect( + htmlArtifactReferences(text).scripts.map(({ scriptPath }) => scriptPath), + ).toEqual(["actual.js"]); +}); diff --git a/src/domain/javascript/htmlArtifactReferences.ts b/src/domain/javascript/htmlArtifactReferences.ts new file mode 100644 index 0000000000..fa9945e365 --- /dev/null +++ b/src/domain/javascript/htmlArtifactReferences.ts @@ -0,0 +1,55 @@ +import { + defaultTreeAdapter, + parse, + type DefaultTreeAdapterTypes, +} from "parse5"; + +interface HtmlScriptReference { + readonly scriptPath: string; + readonly startOffset: number; + readonly endOffset: number; +} + +/** Extract inert HTML script references and the first document base href. */ +export const htmlArtifactReferences = ( + text: string, +): { + readonly baseHref: string | null; + readonly scripts: readonly HtmlScriptReference[]; +} => { + const pending: DefaultTreeAdapterTypes.Node[] = [ + parse(text, { sourceCodeLocationInfo: true }), + ]; + const scripts: HtmlScriptReference[] = []; + let baseHref: string | null = null; + while (pending.length > 0) { + const node = pending.pop(); + if (node === undefined) continue; + if ( + defaultTreeAdapter.isElementNode(node) && + node.namespaceURI === "http://www.w3.org/1999/xhtml" + ) { + if (node.tagName === "base" && baseHref === null) { + const href = node.attrs.find(({ name }) => name === "href"); + if (href !== undefined) baseHref = href.value; + } + if (node.tagName === "script") { + const src = node.attrs.find(({ name }) => name === "src"); + const location = node.sourceCodeLocation?.startTag; + if (src !== undefined && src.value !== "" && location !== undefined) { + scripts.push({ + scriptPath: src.value, + startOffset: location.startOffset, + endOffset: location.endOffset, + }); + } + } + } + // Template content belongs to a separate inert document fragment. Only + // traverse the actual document children, without recursion on deep input. + if ("childNodes" in node) { + for (const child of node.childNodes.toReversed()) pending.push(child); + } + } + return { baseHref, scripts }; +}; diff --git a/src/domain/javascript/javascriptApplicationVersionComparison.ts b/src/domain/javascript/javascriptApplicationVersionComparison.ts index dd340c2986..21052b82e9 100644 --- a/src/domain/javascript/javascriptApplicationVersionComparison.ts +++ b/src/domain/javascript/javascriptApplicationVersionComparison.ts @@ -149,6 +149,8 @@ const comparisonLimitations = ( input: ApplicationVersionComparisonProjectionInput, ): string[] => uniqueSorted([ + ...input.left.graph.limitations, + ...input.right.graph.limitations, "Application comparison never uses bundler module ordinals as persistent identity and performs no fuzzy pairing.", "Exact digest equality proves byte identity only; source-map, structural-fingerprint, and semantic-key matches retain inferred confidence.", "Minified names and chunk locations are not assumed stable across versions.", diff --git a/src/domain/javascript/javascriptExportShapeComparison.ts b/src/domain/javascript/javascriptExportShapeComparison.ts index d8c8620fa4..2d7faa2eda 100644 --- a/src/domain/javascript/javascriptExportShapeComparison.ts +++ b/src/domain/javascript/javascriptExportShapeComparison.ts @@ -1,5 +1,8 @@ -import { uniqueSorted } from "../canonicalOrdering.js"; -import { digestExportShapeValue } from "./javascriptExportShapeComparisonIdentity.js"; +import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { + canonicalExportShapeValue, + digestExportShapeValue, +} from "./javascriptExportShapeComparisonIdentity.js"; import { javaScriptExportShapeComparisonResultSchema, type JavaScriptExportShapeComparisonChange, @@ -14,7 +17,6 @@ import { } from "./javascriptExportShapeSelection.js"; import { buildJavaScriptExportShapeChanges, - compareJavaScriptExportShapeChanges, hasPartialJavaScriptExportPropertyCoverage, pairJavaScriptExportShapeVariants, type JavaScriptExportShapePairing, @@ -42,14 +44,16 @@ export const compareJavaScriptExportShapes = ( input.left.evidenceId, input.right.evidenceId, ); - const allChanges = buildJavaScriptExportShapeChanges({ - leftSelection: left, - rightSelection: right, - pairing, - leftShapes: leftRetained.shapes, - rightShapes: rightRetained.shapes, - evidenceLinks, - }).sort(compareJavaScriptExportShapeChanges); + const allChanges = sortChanges( + buildJavaScriptExportShapeChanges({ + leftSelection: left, + rightSelection: right, + pairing, + leftShapes: leftRetained.shapes, + rightShapes: rightRetained.shapes, + evidenceLinks, + }), + ); const changes = allChanges; const omittedChanges = 0; const coverage = comparisonCoverage({ @@ -62,7 +66,11 @@ export const compareJavaScriptExportShapes = ( allChanges, omittedChanges, }); - const limitations = comparisonLimitations(left, right, coverage.status); + const limitations = uniqueSorted([ + ...input.left.graph.limitations, + ...input.right.graph.limitations, + ...comparisonLimitations(left, right, coverage.status), + ]); const semantic = { left: left.selection, right: right.selection, @@ -78,6 +86,29 @@ export const compareJavaScriptExportShapes = ( }); }; +const sortChanges = ( + changes: readonly JavaScriptExportShapeComparisonChange[], +): JavaScriptExportShapeComparisonChange[] => + changes + .map((change, index) => ({ + change, + index, + sortKey: canonicalExportShapeValue({ + discriminant: change.discriminant, + path: change.path, + status: change.status, + left_source_range: change.left_source_range, + right_source_range: change.right_source_range, + change_id: change.change_id, + }), + })) + .sort( + (left, right) => + compareCodePoints(left.sortKey, right.sortKey) || + left.index - right.index, + ) + .map(({ change }) => change); + interface CoverageInput { readonly input: JavaScriptExportShapeComparisonProjectionInput; readonly left: SelectedJavaScriptExport; diff --git a/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts b/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts index acb1fe1f19..3ab413e858 100644 --- a/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts +++ b/src/domain/javascript/javascriptExportShapeComparisonSchemas.test.ts @@ -1,9 +1,6 @@ import { expect, it } from "vitest"; -import { - javaScriptExportShapeComparisonResultSchema, - projectedExportReturnShapesSchema, -} from "./javascriptExportShapeComparisonSchemas.js"; +import { projectedExportReturnShapesSchema } from "./javascriptExportShapeComparisonSchemas.js"; const projectionWithField = (field: unknown) => ({ semantic_role: "export-return-shapes", @@ -54,104 +51,3 @@ it("parses projected fields into literal, union, or unknown values", () => { ).success, ).toBe(true); }); - -it("accepts every projected return site and field without presentation caps", () => { - const fields = Array.from({ length: 65 }, (_, index) => ({ - path: `/field-${String(index)}`, - state: "literal", - value: index, - reason: null, - })); - const propertyCoverage = fields.map(({ path }) => ({ - path, - status: "complete", - omitted: 0, - })); - const shapes = Array.from({ length: 33 }, () => ({ - source_range: { - start: { line: 1, column: 0 }, - end: { line: 1, column: 10 }, - }, - value_status: "object", - fields, - property_coverage: propertyCoverage, - })); - - expect( - projectedExportReturnShapesSchema.parse({ - ...projectionWithField(fields[0]), - static_return_shapes: shapes, - return_shape_coverage: { - status: "complete", - retained_return_sites: shapes.length, - omitted_return_sites: 0, - omitted_fields: 0, - omitted_property_coverage: 0, - projection_complete: true, - }, - }).static_return_shapes, - ).toHaveLength(33); -}); - -it("accepts complete candidate and change inventories without page ceilings", () => { - const evidenceId = `ev_${"a".repeat(64)}`; - const graphId = `jag_${"b".repeat(64)}`; - const selector = { - evidence_id: evidenceId, - graph_id: graphId, - requested_module_path: "index.js", - requested_export_name: "render", - candidates: Array.from({ length: 1_001 }, (_, index) => ({ - node_id: `jag_node_${index.toString(16).padStart(64, "0")}`, - module_path: "index.js", - export_name: "render", - matches_requested_module: true, - matches_requested_export: true, - })), - omitted_candidates: 0, - status: "missing", - selected_node_id: null, - }; - const changes = Array.from({ length: 10_001 }, (_, index) => ({ - change_id: `jesc_change_${index.toString(16).padStart(64, "0")}`, - status: "unknown", - path: `/field-${String(index)}`, - discriminant: null, - left: { availability: "absent" }, - right: { availability: "absent" }, - left_source_range: null, - right_source_range: null, - evidence_links: [evidenceId, `ev_${"c".repeat(64)}`], - limitations: [], - })); - - expect( - javaScriptExportShapeComparisonResultSchema.parse({ - comparison_id: `jesc_${"d".repeat(64)}`, - left: selector, - right: { ...selector, graph_id: `jag_${"e".repeat(64)}` }, - summary: { added: 0, removed: 0, changed: 0, unknown: changes.length }, - changes, - coverage: { - status: "complete-within-inputs", - left_graph_status: "complete", - right_graph_status: "complete", - paired_variants: 0, - unpaired_left_variants: 0, - unpaired_right_variants: 0, - omitted_left_variants: 0, - omitted_right_variants: 0, - left_source_omitted_variants: 0, - right_source_omitted_variants: 0, - left_omitted_fields: 0, - right_omitted_fields: 0, - left_omitted_property_coverage: 0, - right_omitted_property_coverage: 0, - omitted_candidates: 0, - omitted_changes: 0, - }, - evidence_links: [evidenceId, `ev_${"c".repeat(64)}`], - limitations: [], - }).changes, - ).toHaveLength(10_001); -}); diff --git a/src/domain/javascript/javascriptExportShapeVariants.ts b/src/domain/javascript/javascriptExportShapeVariants.ts index 00c420c446..e5880aca68 100644 --- a/src/domain/javascript/javascriptExportShapeVariants.ts +++ b/src/domain/javascript/javascriptExportShapeVariants.ts @@ -112,16 +112,6 @@ export const buildJavaScriptExportShapeChanges = ( return [...paired, ...unpairedLeft, ...unpairedRight]; }; -/** Sort changes by semantic content rather than traversal order. */ -export const compareJavaScriptExportShapeChanges = ( - left: JavaScriptExportShapeComparisonChange, - right: JavaScriptExportShapeComparisonChange, -): number => - compareCodePoints( - canonicalExportShapeValue(changeSortKey(left)), - canonicalExportShapeValue(changeSortKey(right)), - ); - /** Report whether any retained shape lacks complete property coverage. */ export const hasPartialJavaScriptExportPropertyCoverage = ( shapes: readonly Shape[], @@ -429,15 +419,6 @@ const changeWithId = ( change_id: `jesc_change_${digestExportShapeValue(semantic)}`, }); -const changeSortKey = (change: JavaScriptExportShapeComparisonChange) => ({ - discriminant: change.discriminant, - path: change.path, - status: change.status, - left_source_range: change.left_source_range, - right_source_range: change.right_source_range, - change_id: change.change_id, -}); - const literalValue = ( field: Field, ): diff --git a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts index bc3828fb56..15a6f84454 100644 --- a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts +++ b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts @@ -253,56 +253,58 @@ describe("feature seed selection controls", () => { }); describe("public feature trace from contextBridge source members", () => { - it.each(matchingModes)( - "traces $match / $value / case-sensitive $case_sensitive", - (mode) => { - const graph = contextBridgeGraph(); - const before = structuredClone(graph); - const input = { - sourceEvidenceId, - graph, - nativeEvidence: [], - direction: "both" as const, - }; - const generic = traceApplicationFeature({ - ...input, - seed: { kind: "string", ...mode }, - }); - const typed = traceApplicationFeature({ - ...input, - seed: { kind: "api", ...mode }, - }); + it("traces one exact member seed through the public feature workflow", () => { + const mode = { + value: "openProject", + match: "exact" as const, + case_sensitive: true, + }; + const graph = contextBridgeGraph(); + const before = structuredClone(graph); + const input = { + sourceEvidenceId, + graph, + nativeEvidence: [], + direction: "both" as const, + }; + const generic = traceApplicationFeature({ + ...input, + seed: { kind: "string", ...mode }, + }); + const typed = traceApplicationFeature({ + ...input, + seed: { kind: "api", ...mode }, + }); - expect(typed.coverage).toEqual({ - status: "complete-within-source", - source_graph_status: "complete", - total_seed_matches: 1, - }); - expect(typed.seed_matches).toEqual(generic.seed_matches); - expect(typed.seed_matches).toEqual([ - { - node_id: graph.nodes[0]?.node_id, - kind: "context-bridge-api", - basis: "property", - field: "observations[0].properties.members[1]", - }, - ]); - expect(typed.graph?.nodes).toEqual(graph.nodes); - expect(typed.graph?.edges).toEqual([]); - expect(typed.evidence_links).toEqual([sourceEvidenceId]); - expect(typed.summary).toMatchObject({ - matched_seeds: 1, - traced_nodes: 1, - traced_edges: 0, - observed_facts: 1, - inferred_facts: 0, - unknown_facts: 0, - unavailable_facts: 0, - }); - expect(typed.native_handoffs).toEqual([]); - expect(graph).toEqual(before); - }, - ); + expect(typed.coverage).toEqual({ + status: "complete-within-source", + source_graph_status: "complete", + total_seed_matches: 1, + }); + expect(typed.seed_matches).toEqual(generic.seed_matches); + expect(typed.seed_matches).toEqual([ + { + node_id: graph.nodes[0]?.node_id, + kind: "context-bridge-api", + basis: "property", + field: "observations[0].properties.members[1]", + }, + ]); + expect(typed.graph?.nodes).toEqual(graph.nodes); + expect(typed.graph?.edges).toEqual([]); + expect(typed.evidence_links).toEqual([sourceEvidenceId]); + expect(typed.summary).toMatchObject({ + matched_seeds: 1, + traced_nodes: 1, + traced_edges: 0, + observed_facts: 1, + inferred_facts: 0, + unknown_facts: 0, + unavailable_facts: 0, + }); + expect(typed.native_handoffs).toEqual([]); + expect(graph).toEqual(before); + }); it("keeps a missing member explicit without changing source coverage", () => { const graph = contextBridgeGraph(); diff --git a/src/domain/javascript/javascriptFeatureTrace.ts b/src/domain/javascript/javascriptFeatureTrace.ts index c10b2bd258..7c1c49589a 100644 --- a/src/domain/javascript/javascriptFeatureTrace.ts +++ b/src/domain/javascript/javascriptFeatureTrace.ts @@ -119,6 +119,7 @@ const noMatchResult = ( evidence_links: [input.sourceEvidenceId], limitations: uniqueSorted([ "No graph entity matched the literal seed; this is not evidence that the feature is absent.", + ...input.graph.limitations, ...sourceCoverageLimitations(input.graph), ]), }; @@ -239,6 +240,7 @@ const traceCoverageStatus = ( const traceLimitations = (graph: JavaScriptApplicationGraph): string[] => uniqueSorted([ + ...graph.limitations, "A trace reports graph relationships, not proof that code executed or that a feature is reachable in every state.", "Static, native, passive-runtime, inferred, and unknown facts retain their original authority in the returned graph.", "Native handoffs never open a binary or invoke a provider automatically; snapshot reuse remains provider/profile/target exact.", diff --git a/src/domain/javascript/javascriptRecovery.test.ts b/src/domain/javascript/javascriptRecovery.test.ts index 5f9a2b687b..4a48a44b00 100644 --- a/src/domain/javascript/javascriptRecovery.test.ts +++ b/src/domain/javascript/javascriptRecovery.test.ts @@ -2,19 +2,6 @@ import { describe, expect, it } from "vitest"; import { javascriptRecoveryInputSchema } from "./javascriptRecovery.js"; describe("JavaScript recovery requests", () => { - it("defaults omitted transform metadata without requiring engine confirmations", () => { - expect( - javascriptRecoveryInputSchema.parse({ - path: "/tmp/bundle.js", - output_directory: "/tmp/recovered", - }), - ).toEqual({ - path: "/tmp/bundle.js", - output_directory: "/tmp/recovered", - extraction_mode: "structural", - rewrite_level: "standard", - }); - }); it.each([ { path: "", output_directory: "/tmp/recovered" }, { path: "/tmp/bundle.js" }, diff --git a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts index 0e4b2c22a5..15a4619c3b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts @@ -1,39 +1,15 @@ import { expect, it } from "vitest"; + import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; -it.each(["namespace", "default", "named-default"])( - "retains Node built-in effects from %s imports", - (mode) => { - const imports = ["fs", "child_process", "timers", "http"] - .map((name, index) => { - const declaration = - mode === "namespace" - ? `* as m${String(index)}` - : mode === "default" - ? `m${String(index)}` - : `{default as m${String(index)}}`; - return `import ${declaration} from 'node:${name}';`; - }) - .join("\n"); - const ir = analyzeJavaScriptSemantics(`${imports} - const text = m0.readFileSync('config.json','utf8'); - const stream = m0.createReadStream('config.json'); stream.destroy(); - const child = m1.spawn('/usr/bin/tool', ['--version']); - const timer = m2.setTimeout(work,1000); m2.clearTimeout(timer); - const request = m3.get('http://localhost/'); +it("does not assign built-in APIs to unrelated or shadowed bindings", () => { + const ir = analyzeJavaScriptSemantics(` + import fs from "./fs.js"; + import { constants as constantsFs } from "node:fs"; + fs.createReadStream("config.json"); + constantsFs.createReadStream("config.json"); + function run(fs) { fs.createReadStream("config.json"); } `); - expect(ir.configurationOperations).toHaveLength(1); - expect(ir.resourceOperations).toHaveLength(2); - expect(ir.childProcessSpawns).toHaveLength(1); - expect(ir.timerOperations).toHaveLength(2); - expect(ir.requestOperations).toHaveLength(1); - }, -); -it.each([ - "import fs from './fs.js'; fs.createReadStream('config.json');", - "import {constants as fs} from 'node:fs'; fs.createReadStream('config.json');", - "function run(fs) { fs.createReadStream('config.json'); }", -])("does not invent a built-in namespace for %s", (source) => { - expect(analyzeJavaScriptSemantics(source).resourceOperations).toEqual([]); + expect(ir.resourceOperations).toEqual([]); }); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts index 78fea98456..880aae06ce 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts @@ -155,19 +155,6 @@ describe("JavaScript semantic analysis: calls 2", () => { ]); }); - it("retains every promise operation found in the finite source", () => { - const ir = analyzeJavaScriptSemantics( - ` - Promise.resolve(1); - Promise.resolve(2); - Promise.resolve(3); - `, - ); - - expect(ir.promiseOperations).toHaveLength(3); - expect(ir.coverage.status).toBe("complete"); - }); - it("fingerprints formatting and local-name changes identically", () => { const left = analyzeJavaScriptSemantics(` function calculate(value) { diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts index 9d27e0ed5d..e9f8d8159b 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts @@ -1,67 +1,43 @@ import * as t from "@babel/types"; import { expect, it } from "vitest"; -import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { + analyzeJavaScriptSemantics, + analyzeParsedJavaScriptSemantics, +} from "./javascriptSemanticAnalysis.js"; import { onlyBinding, origin } from "./javascriptSemanticAnalysis.fixture.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { calleeName } from "./javascriptStaticAnalysisHelpers.js"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; const memberChain = ".next".repeat(12_000); +const ordinaryMemberChain = ".next".repeat(64); -it("retains native addon observations through a deep member initializer", () => { - const analysis = analyzeJavaScriptStaticSource( - `const value = require("./addon.node")${memberChain}.last[""];`, +it("preserves a 12,000-member native import through both analyzers", () => { + const parsed = parseJavaScriptSource( + `const value = require("./addon.node")${memberChain}.last; const ordinary = root.next.last;`, ); - expect(analysis.electron.native_addon_bindings).toEqual([ + if (parsed === null) throw new Error("Expected valid JavaScript"); + + const staticAnalysis = analyzeParsedJavaScriptStaticSource("", parsed); + const ir = analyzeParsedJavaScriptSemantics(parsed); + expect(staticAnalysis.parse_status).toBe("complete"); + expect(staticAnalysis.electron.native_addon_bindings).toEqual([ expect.objectContaining({ specifier: "./addon.node", members: ["last"] }), ]); - expect(analysis.parse_error_count).toBe(0); -}); - -it("keeps a deep non-native initializer outside native addon observations", () => { - const analysis = analyzeJavaScriptStaticSource( - `const value = root${memberChain}.last;`, - ); - expect(analysis.electron.native_addon_bindings).toEqual([]); - expect(analysis.parse_status).toBe("complete"); -}); - -it("retains unknown value diagnostics and references after a deep member initializer", () => { - const ir = analyzeJavaScriptSemantics( - `const value = root${memberChain}.last;`, - ); expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 }); - expect(onlyBinding(ir, "value")).toMatchObject({ - value: { status: "unknown", reason: "Cannot project last from unknown." }, - provenance: { status: "unknown", reason: "Unbound identifier root." }, - }); - expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([ - { name: "root", role: "read" }, - ]); -}); - -it.each([ - `const value = require("./dependency.js")${memberChain}[""].last;`, - `const dependency = require("./dependency.js"); const value = dependency${memberChain}[""].last;`, - `const value = require("./dependency.js")?.next${memberChain}[""].last;`, -])("preserves the full literal module path (case %#)", (source) => { - const ir = analyzeJavaScriptSemantics(source); const path = origin(onlyBinding(ir, "value")); - expect(path.specifier).toBe("./dependency.js"); + expect(path.specifier).toBe("./addon.node"); expect(path.importedPath).toEqual([ - ...(source.includes("?.next") ? ["next"] : []), ...Array.from({ length: 12_000 }, () => "next"), - "", "last", ]); - expect(ir.coverage.status).toBe("complete"); }); it("retains a deep CommonJS re-export path without inventing dynamic module links", () => { const ir = analyzeJavaScriptSemantics(` - module.exports.result = require("./dependency.js")${memberChain}.last; - const dynamic = require("./other.js")${memberChain}[key].last; + module.exports.result = require("./dependency.js")${ordinaryMemberChain}.last; + const dynamic = require("./other.js")${ordinaryMemberChain}[key].last; `); expect(ir.moduleLinks).toEqual([ expect.objectContaining({ @@ -80,7 +56,7 @@ it("retains a deep CommonJS re-export path without inventing dynamic module link it("invalidates a deep mutation while retaining unrelated literal properties", () => { const ir = analyzeJavaScriptSemantics(` const root = { untouched: "retained" }; - root${memberChain}.last = 1; + root${ordinaryMemberChain}.last = 1; const value = root.untouched; `); expect(onlyBinding(ir, "root").value).toEqual({ @@ -98,7 +74,7 @@ it("invalidates a deep mutation while retaining unrelated literal properties", ( }); it("retains the complete location and unresolved target of a deep call", () => { - const callee = `root${memberChain}.last`; + const callee = `root${ordinaryMemberChain}.last`; const ir = analyzeJavaScriptSemantics(`${callee}();`); expect(ir.callSites).toEqual([ expect.objectContaining({ @@ -115,7 +91,7 @@ it("retains the complete location and unresolved target of a deep call", () => { it("retains deep default values without confusing their reads with parameter bindings", () => { const ir = analyzeJavaScriptSemantics( - `export function inspect(value = root${memberChain}.last) { return value; }`, + `export function inspect(value = root${ordinaryMemberChain}.last) { return value; }`, ); expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([ { name: "root", role: "read" }, @@ -126,7 +102,7 @@ it("retains deep default values without confusing their reads with parameter bin it("keeps a shadowed require local through a deep member initializer", () => { const ir = analyzeJavaScriptSemantics( - `function inspect(require) { const value = require("./dependency.js")${memberChain}.last; return value; }`, + `function inspect(require) { const value = require("./dependency.js")${ordinaryMemberChain}.last; return value; }`, ); expect(ir.moduleLinks).toEqual([]); expect(onlyBinding(ir, "value").provenance.origins).toEqual([]); @@ -140,7 +116,7 @@ it.each([ ["root[0].last()", "root.0.last"], ["root[key].last()", "root.[computed@5].last"], ["root?.first.last()", "root.first.last"], - [`root${memberChain}.last()`, `root${memberChain}.last`], + [`root${ordinaryMemberChain}.last()`, `root${ordinaryMemberChain}.last`], ])("preserves exact callee syntax (case %#)", (source, expected) => { const file = parseJavaScriptSource(source); const statement = file?.program.body[0]; diff --git a/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts index 66d4845069..58b572a961 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts @@ -147,3 +147,54 @@ it("keeps an explicit return statement's full range and the producer's location" }); expect(site.location.start.column).toBe(source.indexOf("return")); }); + +it("links returned promises to the exact return site in each callable", () => { + const ir = analyzeJavaScriptSemantics( + [ + "function first() { return Promise.resolve(1); }", + "function second() { return Promise.resolve(2); }", + ].join("\n"), + ); + const callables = new Map( + ir.callables.map((callable) => [callable.name, callable]), + ); + const promises = ir.promiseOperations.filter( + ({ method }) => method === "resolve", + ); + + expect(promises).toHaveLength(2); + for (const name of ["first", "second"] as const) { + const callable = callables.get(name); + const promise = promises.find( + ({ ownerCallableId }) => ownerCallableId === callable?.callableId, + ); + expect(callable?.returnSites).toHaveLength(1); + expect(promise).toMatchObject({ + ownership: "returned", + ownerCallableId: callable?.callableId, + returnSiteId: callable?.returnSites[0]?.returnSiteId, + }); + } +}); + +it("does not assign a return site to a Promise owned by a binding", () => { + const ir = analyzeJavaScriptSemantics( + "function unrelated() { return Promise.resolve(2); }\nconst pending = Promise.resolve(1);", + ); + const owned = ir.promiseOperations.find( + ({ ownerCallableId }) => ownerCallableId !== null, + ); + const assigned = ir.promiseOperations.find( + ({ ownerCallableId }) => ownerCallableId === null, + ); + + expect(owned).toMatchObject({ + ownership: "returned", + returnSiteId: ir.callables.find(({ name }) => name === "unrelated") + ?.returnSites[0]?.returnSiteId, + }); + expect(assigned).toMatchObject({ + ownership: "assigned", + returnSiteId: null, + }); +}); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts index 25e02aefad..e6a06e7284 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts @@ -18,6 +18,19 @@ const valueOf = (expression: string) => analyzeJavaScriptSemantics(`const answer = ${expression};`), "answer", ).value; +const valuesOf = (expressions: readonly string[]) => { + const ir = analyzeJavaScriptSemantics( + expressions + .map( + (expression, index) => + `const candidate${String(index)} = ${expression};`, + ) + .join("\n"), + ); + return expressions.map( + (_, index) => topLevelBinding(ir, `candidate${String(index)}`).value, + ); +}; describe("conditional primitive unions", () => { const cases = conditionalCandidates.flatMap((known) => @@ -35,11 +48,11 @@ describe("conditional primitive unions", () => { ]), ); - it.each(cases)("preserves incompatible alternatives in %s", (expression) => { - expect(valueOf(expression)).toEqual(incompatibleValue); + it("preserves incompatible alternatives across conditional expressions", () => { + expect(valuesOf(cases)).toEqual(cases.map(() => incompatibleValue)); }); - it.each([ + const exactCases = [ [ 'condition ? (choice ? "two" : "one") : "three"', { status: "union", values: ["one", "three", "two"] }, @@ -69,8 +82,12 @@ describe("conditional primitive unions", () => { { status: "union", values: [null, "one", "two"] }, ], ["condition ? null : null", { status: "literal", value: null }], - ])("retains exact primitive candidates in %s", (expression, expected) => { - expect(valueOf(expression)).toEqual(expected); + ] as const; + + it("retains exact primitive candidates across conditional expressions", () => { + expect(valuesOf(exactCases.map(([expression]) => expression))).toEqual( + exactCases.map(([, expected]) => expected), + ); }); it("propagates an incompatible nested branch through an outer union", () => { @@ -119,11 +136,11 @@ describe("logical primitive unions", () => { ), ); - it.each(cases)("preserves unresolved alternatives in %s", (expression) => { - expect(valueOf(expression)).toEqual(incompatibleValue); + it("preserves unresolved alternatives across logical expressions", () => { + expect(valuesOf(cases)).toEqual(cases.map(() => incompatibleValue)); }); - it.each([ + const exactCases = [ [ '(choice ? "" : "one") || "two"', { status: "union", values: ["", "one", "two"] }, @@ -138,19 +155,24 @@ describe("logical primitive unions", () => { ], ['"one" || "one"', { status: "literal", value: "one" }], ["null ?? null", { status: "literal", value: null }], - ])( - "retains the conservative primitive union in %s", - (expression, expected) => { - expect(valueOf(expression)).toEqual(expected); - }, - ); + ] as const; - it.each([ + it("retains conservative primitive unions across logical expressions", () => { + expect(valuesOf(exactCases.map(([expression]) => expression))).toEqual( + exactCases.map(([, expected]) => expected), + ); + }); + + const unevaluatedCases = [ 'true ? "one" : missing', '"one" || missing', "false && missing", '"one" ?? missing', - ])("does not introduce short-circuit evaluation for %s", (expression) => { - expect(valueOf(expression)).toEqual(incompatibleValue); + ]; + + it("does not introduce short-circuit evaluation", () => { + expect(valuesOf(unevaluatedCases)).toEqual( + unevaluatedCases.map(() => incompatibleValue), + ); }); }); diff --git a/src/domain/javascript/javascriptSemanticGraph.ts b/src/domain/javascript/javascriptSemanticGraph.ts index a7af45a995..86884cba7c 100644 --- a/src/domain/javascript/javascriptSemanticGraph.ts +++ b/src/domain/javascript/javascriptSemanticGraph.ts @@ -38,7 +38,8 @@ const normalizeEvidence = ( evidence_ids: uniqueSorted(evidence.evidence_ids), }); -const nodeId = ( +/** Derive a semantic node ID from its kind and exact artifact identity. */ +export const javaScriptSemanticNodeId = ( node: Pick, ): string => `jsrg_node_${canonicalDigest({ kind: node.kind, identity: node.identity }, "JavaScript semantic graph")}`; @@ -59,7 +60,7 @@ export const createJavaScriptSemanticGraphNode = ( }; return javaScriptSemanticNodeSchema.parse({ ...semantic, - node_id: nodeId(semantic), + node_id: javaScriptSemanticNodeId(semantic), }); }; @@ -129,11 +130,21 @@ export const createJavaScriptSemanticFingerprint = (input: unknown) => { type GraphRecord = z.infer; +type SemanticGraphIssue = { + readonly code: "custom"; + readonly path: PropertyKey[]; + readonly message: string; +}; + +interface GraphIssueReporter { + readonly addIssue: (issue: SemanticGraphIssue) => void; +} + const sortedUniqueIssue = ( values: readonly string[], path: PropertyKey[], label: string, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { for (let index = 1; index < values.length; index += 1) { if (compareCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0) @@ -147,7 +158,10 @@ const sortedUniqueIssue = ( } }; -const checkCoverage = (graph: GraphRecord, context: z.RefinementCtx): void => { +const checkCoverage = ( + graph: GraphRecord, + context: GraphIssueReporter, +): void => { const families = graph.coverage.families.map(({ family }) => family); if ( canonicalJson(families, "JavaScript semantic graph") !== @@ -221,7 +235,7 @@ const checkCoverage = (graph: GraphRecord, context: z.RefinementCtx): void => { const checkCanonicalOrder = ( graph: GraphRecord, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { sortedUniqueIssue( graph.root_node_ids, @@ -259,10 +273,10 @@ const checkCanonicalOrder = ( const checkNodes = ( graph: GraphRecord, nodes: ReadonlyMap, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { for (const [index, node] of graph.nodes.entries()) { - if (node.node_id !== nodeId(node)) + if (node.node_id !== javaScriptSemanticNodeId(node)) context.addIssue({ code: "custom", path: ["nodes", index, "node_id"], @@ -283,7 +297,7 @@ const checkNodes = ( const checkRoots = ( graph: GraphRecord, nodes: ReadonlyMap, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { for (const root of graph.root_node_ids) if (!nodes.has(root)) @@ -297,7 +311,7 @@ const checkRoots = ( const checkRelations = ( graph: GraphRecord, nodes: ReadonlyMap, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { for (const [index, relation] of graph.relations.entries()) { const { relation_id: identifier, ...semantic } = relation; @@ -340,7 +354,7 @@ const checkRelations = ( const checkUnknowns = ( graph: GraphRecord, nodes: ReadonlyMap, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { for (const [index, unknown] of graph.unknowns.entries()) { const { unknown_id: identifier, ...semantic } = unknown; @@ -386,7 +400,7 @@ const checkUnknowns = ( const checkUnknownReferences = ( graph: GraphRecord, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { const unknownIds = new Set( graph.unknowns.map(({ unknown_id }) => unknown_id), @@ -403,7 +417,7 @@ const checkUnknownReferences = ( const checkFingerprints = ( graph: GraphRecord, nodes: ReadonlyMap, - context: z.RefinementCtx, + context: GraphIssueReporter, ): void => { for (const [index, fingerprint] of graph.fingerprints.entries()) { if (nodes.get(fingerprint.function_node_id)?.kind !== "function") @@ -449,7 +463,10 @@ const checkFingerprints = ( } }; -const checkGraph = (graph: GraphRecord, context: z.RefinementCtx): void => { +const checkGraphContent = ( + graph: GraphRecord, + context: GraphIssueReporter, +): void => { checkCanonicalOrder(graph, context); const nodes = new Map(graph.nodes.map((node) => [node.node_id, node])); checkNodes(graph, nodes, context); @@ -465,6 +482,10 @@ const checkGraph = (graph: GraphRecord, context: z.RefinementCtx): void => { path: ["limitations"], message: "Non-complete graph coverage requires a limitation", }); +}; + +const checkGraph = (graph: GraphRecord, context: GraphIssueReporter): void => { + checkGraphContent(graph, context); const { graph_id: identifier, ...semantic } = graph; if ( identifier !== @@ -523,10 +544,21 @@ export const createJavaScriptSemanticGraph = ( }, limitations: uniqueSorted(parsed.limitations), }; - return javaScriptSemanticGraphSchema.parse({ + // The input schema already cloned and validated every field. Validate the + // normalized relationships in place, then derive the only added field. The + // public schema still verifies arbitrary records and their commitments. + const record: GraphRecord = { ...semantic, graph_id: `jsrg_${canonicalDigest(semantic, "JavaScript semantic graph")}`, + }; + const issues: SemanticGraphIssue[] = []; + checkGraphContent(record, { + addIssue: (issue) => { + issues.push(issue); + }, }); + if (issues.length > 0) throw new z.ZodError(issues); + return record; }; export type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation }; diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 873d3491e3..31510e6c8d 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -15,7 +15,7 @@ import { } from "./javascriptSemanticState.js"; import { compareCodePoints } from "../canonicalOrdering.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { range, sourceRangesEqual } from "./javascriptStaticAnalysisHelpers.js"; +import { range } from "./javascriptStaticAnalysisHelpers.js"; import { assignedSemanticResultBindings, dataEffectMemberCallee, @@ -56,7 +56,10 @@ interface PromiseOwnershipContext { readonly candidate: PromiseCandidate; readonly candidateByNode: WeakMap; readonly state: JavaScriptSemanticAnalysisState; - readonly callables: readonly JavaScriptSemanticCallable[]; + readonly returnSiteIdFor: ( + callableId: string | null, + location: JavaScriptSemanticPromiseOperation["location"], + ) => string | null; } /** Recover explicit Promise construction, chaining, aggregation, and ownership. */ @@ -66,6 +69,7 @@ export const collectJavaScriptSemanticPromises = ( callables: readonly JavaScriptSemanticCallable[], ): JavaScriptSemanticPromiseOperation[] => { const candidates = collectCandidates(program, state, callables); + const returnSiteIdFor = createReturnSiteLookup(callables); const candidateByNode = new WeakMap(); for (const candidate of candidates) candidateByNode.set(candidate.node, candidate); @@ -74,7 +78,7 @@ export const collectJavaScriptSemanticPromises = ( candidate, candidateByNode, state, - callables, + returnSiteIdFor, ); const sources = promiseSources(candidate, candidateByNode, state); return { @@ -180,14 +184,14 @@ const promiseOwnership = ( candidate: PromiseCandidate, candidateByNode: WeakMap, state: JavaScriptSemanticAnalysisState, - callables: readonly JavaScriptSemanticCallable[], + returnSiteIdFor: PromiseOwnershipContext["returnSiteIdFor"], ): PromiseOwnership => { if (candidate.kind === "awaited-expression") return emptyOwnership("awaited"); const context: PromiseOwnershipContext = { candidate, candidateByNode, state, - callables, + returnSiteIdFor, }; for (let index = candidate.ancestors.length - 1; index >= 0; index -= 1) { const ancestor = candidate.ancestors[index]; @@ -203,7 +207,7 @@ const ownershipAtAncestor = ( ancestor: t.Node, context: PromiseOwnershipContext, ): PromiseOwnership | "boundary" | null => { - const { candidate, candidateByNode, state, callables } = context; + const { candidate, candidateByNode, state, returnSiteIdFor } = context; if (t.isAwaitExpression(ancestor)) return unwrapExpression(ancestor.argument) === candidate.node ? emptyOwnership("awaited") @@ -234,7 +238,7 @@ const ownershipAtAncestor = ( return returnedPromiseOwnership( candidate.ownerCallableId, range(ancestor), - callables, + returnSiteIdFor, ); } if (t.isExpressionStatement(ancestor)) @@ -249,7 +253,7 @@ const ownershipAtAncestor = ( ? returnedPromiseOwnership( candidate.ownerCallableId, range(ancestor.body), - callables, + returnSiteIdFor, ) : "boundary"; }; @@ -283,11 +287,11 @@ const assignedPromiseBinding = ( const returnedPromiseOwnership = ( callableId: string | null, location: JavaScriptSemanticPromiseOperation["location"], - callables: readonly JavaScriptSemanticCallable[], + returnSiteIdFor: PromiseOwnershipContext["returnSiteIdFor"], ): PromiseOwnership => ({ ownership: "returned", ownerBindingId: null, - returnSiteId: matchingReturnSite(callableId, location, callables), + returnSiteId: returnSiteIdFor(callableId, location), }); const outerConsumesCandidate = ( @@ -329,16 +333,56 @@ const emptyOwnership = ( returnSiteId: null, }); -const matchingReturnSite = ( - callableId: string | null, - location: JavaScriptSemanticPromiseOperation["location"], +const indexReturnSites = ( + callable: JavaScriptSemanticCallable, +): ReadonlyMap => { + const output = new Map(); + for (const site of callable.returnSites) { + const key = returnSiteKey(site.location); + if (!output.has(key)) output.set(key, site.returnSiteId); + } + return output; +}; + +const createReturnSiteLookup = ( callables: readonly JavaScriptSemanticCallable[], -): string | null => - callables - .find(({ callableId: candidate }) => candidate === callableId) - ?.returnSites.find(({ location: candidate }) => - sourceRangesEqual(candidate, location), - )?.returnSiteId ?? null; +): PromiseOwnershipContext["returnSiteIdFor"] => { + let callablesById: + | ReadonlyMap + | undefined; + let returnSiteIdsByCallable: + | Map> + | undefined; + return (callableId, location) => { + if (callableId === null) return null; + callablesById ??= indexCallablesById(callables); + const callable = callablesById.get(callableId); + if (callable === undefined) return null; + returnSiteIdsByCallable ??= new Map(); + let returnSiteIds = returnSiteIdsByCallable.get(callableId); + if (returnSiteIds === undefined) { + returnSiteIds = indexReturnSites(callable); + returnSiteIdsByCallable.set(callableId, returnSiteIds); + } + return returnSiteIds.get(returnSiteKey(location)) ?? null; + }; +}; + +const indexCallablesById = ( + callables: readonly JavaScriptSemanticCallable[], +): ReadonlyMap => { + const output = new Map(); + for (const callable of callables) { + if (output.has(callable.callableId)) continue; + output.set(callable.callableId, callable); + } + return output; +}; + +const returnSiteKey = ( + location: JavaScriptSemanticPromiseOperation["location"], +): string => + `${location.start.line}:${location.start.column}:${location.end.line}:${location.end.column}`; const promiseSources = ( candidate: PromiseCandidate, diff --git a/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts b/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts index a4f3341633..ab64cfe007 100644 --- a/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts +++ b/src/domain/javascript/javascriptSemanticQuerySchemas.test.ts @@ -1,28 +1,8 @@ import { expect, it } from "vitest"; -import { - javaScriptSemanticQueryInputSchema, - javaScriptSemanticQueryResultSchema, -} from "./javascriptSemanticQuerySchemas.js"; +import { javaScriptSemanticQueryInputSchema } from "./javascriptSemanticQuerySchemas.js"; -it("accepts long selectors and complete relation and class selections", () => { - const longText = "x".repeat(16_385); - const result = javaScriptSemanticQueryInputSchema.parse({ - seed: { kind: "endpoint", value: longText }, - direction: "forward-influence", - allowed_relations: Array(2).fill("calls"), - expected: { - role: "sink", - classes: Array(25).fill("module"), - }, - }); - - expect(result.seed).toEqual({ kind: "endpoint", value: longText }); - expect(result.allowed_relations).toHaveLength(2); - expect(result.expected?.classes).toHaveLength(25); -}); - -it("rejects the inert source-map authority setting", () => { +it("rejects a source-map authority setting that the query does not consume", () => { expect( javaScriptSemanticQueryInputSchema.safeParse({ seed: { kind: "endpoint", value: "/api" }, @@ -31,36 +11,3 @@ it("rejects the inert source-map authority setting", () => { }).success, ).toBe(false); }); - -it("accepts every reported limitation without a count ceiling", () => { - const limitations = Array.from( - { length: 1_001 }, - (_, index) => `gap-${index}`, - ); - const id = "a".repeat(64); - - const result = javaScriptSemanticQueryResultSchema.parse({ - query_id: `jsrq_${id}`, - source_graph_id: `jsrg_${id}`, - seed: { kind: "literal", value: null }, - direction: "forward-influence", - status: "no-match", - seed_node_ids: [], - nodes: [], - relations: [], - unknowns: [], - expected_match_node_ids: [], - summary: { - total_seed_matches: 0, - traversed_nodes: 0, - traversed_relations: 0, - traversed_functions: 0, - traversed_modules: 0, - relevant_unknowns: 0, - }, - coverage: { status: "complete" }, - limitations, - }); - - expect(result.limitations).toHaveLength(1_001); -}); diff --git a/src/domain/javascript/javascriptSemanticQuerySchemas.ts b/src/domain/javascript/javascriptSemanticQuerySchemas.ts index 0e9faf4f6a..0cd94e2806 100644 --- a/src/domain/javascript/javascriptSemanticQuerySchemas.ts +++ b/src/domain/javascript/javascriptSemanticQuerySchemas.ts @@ -12,16 +12,21 @@ import { digestSchema } from "../digests.js"; import { prefixedDigestSchema } from "../digests.js"; const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node"); +const literalValueSchema = jsonValueSchema.refine( + (value) => + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean", + "Literal seeds accept only JSON primitive values", +); +// Zod derivations drop the shared schema's JSON Schema projection override; +// re-apply it so MCP catalog generation keeps projecting `{}`. +literalValueSchema._zod.toJSONSchema = () => ({}); + const literalSeedSchema = z.strictObject({ kind: z.literal("literal"), - value: jsonValueSchema.refine( - (value) => - value === null || - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean", - "Literal seeds accept only JSON primitive values", - ), + value: literalValueSchema, }); /** Authenticated starting points accepted by semantic tracing. */ diff --git a/src/domain/javascript/javascriptSemanticTraversal.test.ts b/src/domain/javascript/javascriptSemanticTraversal.test.ts deleted file mode 100644 index 482afe2b94..0000000000 --- a/src/domain/javascript/javascriptSemanticTraversal.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as t from "@babel/types"; -import { expect, it } from "vitest"; - -import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; - -it("preserves source-tree enter/exit order and detached ancestor snapshots", () => { - const parsed = parseJavaScriptSource("const answer = first + second;"); - if (parsed === null) - throw new TypeError("Expected a parsed JavaScript fixture"); - const visits: string[] = []; - const snapshots: (readonly t.Node[])[] = []; - traverseJavaScriptAst(parsed.program, { - enter: (node, parent, readAncestors) => { - visits.push(`enter:${node.type}`); - const ancestors = readAncestors(); - expect(ancestors.at(-1) ?? null).toBe(parent); - if (t.isIdentifier(node)) snapshots.push(ancestors); - }, - exit: (node) => visits.push(`exit:${node.type}`), - }); - expect(visits).toEqual([ - "enter:Program", - "enter:VariableDeclaration", - "enter:VariableDeclarator", - "enter:Identifier", - "exit:Identifier", - "enter:BinaryExpression", - "enter:Identifier", - "exit:Identifier", - "enter:Identifier", - "exit:Identifier", - "exit:BinaryExpression", - "exit:VariableDeclarator", - "exit:VariableDeclaration", - "exit:Program", - ]); - expect(snapshots.map((nodes) => nodes.map(({ type }) => type))).toEqual([ - ["Program", "VariableDeclaration", "VariableDeclarator"], - [ - "Program", - "VariableDeclaration", - "VariableDeclarator", - "BinaryExpression", - ], - [ - "Program", - "VariableDeclaration", - "VariableDeclarator", - "BinaryExpression", - ], - ]); -}); - -it("skips sparse child slots without disturbing sibling visit order", () => { - const parsed = parseJavaScriptSource("const values = [first, , second];"); - if (parsed === null) - throw new TypeError("Expected a parsed JavaScript fixture"); - const identifiers: string[] = []; - traverseJavaScriptAst(parsed.program, { - enter: (node) => { - if (t.isIdentifier(node)) identifiers.push(node.name); - }, - }); - expect(identifiers).toEqual(["values", "first", "second"]); -}); diff --git a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts index 6e426c7c68..3d03133013 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts @@ -5,6 +5,7 @@ import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; const memberChain = ".next".repeat(12_000); +const ordinaryMemberChain = ".next".repeat(64); it("collects a 12,000-member CommonJS export path without recursion", () => { const file = parseJavaScriptSource(`exports${memberChain}.last = 1;`); @@ -48,8 +49,8 @@ it("keeps left-first and outer-member-first bundler runtime matches", () => { ).toBe("webpackChunkInner"); }); -it("analyzes a 12,000-member bundler runtime chain without recursion", () => { - const source = `globalThis.webpackChunkApp${memberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`; +it("recognizes a bundler runtime through nested member names", () => { + const source = `globalThis.webpackChunkApp${ordinaryMemberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`; const analysis = analyzeJavaScriptStaticSource(source); expect(analysis.parse_status).toBe("complete"); expect(analysis.parse_error_count).toBe(0); diff --git a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts index d9ae953805..93a9ddec90 100644 --- a/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisSourceMaps.test.ts @@ -22,12 +22,6 @@ describe("source map directives", () => { expect(sourceMapUrls(source)).toEqual([expected]); }); - it("keeps the comment terminator out of the declared url", () => { - expect(sourceMapUrls("/*# sourceMappingURL=app.js.map */")).toEqual([ - "app.js.map", - ]); - }); - it("retains every directive in a source", () => { expect( sourceMapUrls( @@ -84,30 +78,6 @@ describe("source map comment boundaries", () => { }); describe("source map comment locations and recovery", () => { - it("keeps exact locations and the first occurrence of duplicate URLs", () => { - const line = "//# sourceMappingURL=first.map"; - const block = "/*# sourceMappingURL=second.map"; - const result = analyzeJavaScriptStaticSource( - ['const marker = "😀";', line, `${block} */`, line].join("\r\n"), - ); - expect(result.source_map_urls).toEqual([ - { - declared_url: "first.map", - location: { - start: { line: 2, column: 0 }, - end: { line: 2, column: line.length }, - }, - }, - { - declared_url: "second.map", - location: { - start: { line: 3, column: 0 }, - end: { line: 3, column: block.length }, - }, - }, - ]); - }); - it("keeps real comments when the parser recovers and reports partial syntax", () => { const result = analyzeJavaScriptStaticSource( "let value; let value;\n//# sourceMappingURL=recovered.map", diff --git a/src/domain/jsonValue.test.ts b/src/domain/jsonValue.test.ts new file mode 100644 index 0000000000..9b59312338 --- /dev/null +++ b/src/domain/jsonValue.test.ts @@ -0,0 +1,132 @@ +import { Ajv2020 } from "ajv/dist/2020.js"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { + jsonObjectSchema, + jsonValueSchema, + MAX_JSON_DEPTH, +} from "./jsonValue.js"; + +const values = [ + "text", + 0, + false, + null, + [], + {}, + { nested: [{ deeper: [1, "two", null, { three: true }] }] }, +]; + +it("projects JSON values without recursive definitions", () => { + for (const io of ["input", "output"] as const) { + const schema = z.toJSONSchema( + z.object({ value: jsonValueSchema, map: jsonObjectSchema }), + { io }, + ); + expect(JSON.stringify(schema)).not.toContain("$ref"); + expect(schema).not.toHaveProperty("$defs"); + const validate = new Ajv2020({ strict: false }).compile(schema); + for (const value of values) + expect(validate({ value, map: { value } })).toBe(true); + expect(validate({ value: 1, map: [] })).toBe(false); + } +}); + +it("still parses only JSON values at runtime", () => { + for (const value of values) + expect(jsonValueSchema.safeParse(value).success).toBe(true); + for (const value of [undefined, Number.NaN, Infinity, () => 0, [undefined]]) + expect(jsonValueSchema.safeParse(value).success).toBe(false); +}); + +const deepObject = (depth: number): unknown => { + let value: unknown = 1; + for (let index = 0; index < depth; index += 1) value = { nested: value }; + return value; +}; + +describe("jsonValueSchema depth bound", () => { + it("accepts values nested to the maximum depth", () => { + expect(jsonValueSchema.safeParse(deepObject(MAX_JSON_DEPTH)).success).toBe( + true, + ); + }); + + it("rejects values nested past the maximum depth without throwing", () => { + const result = jsonValueSchema.safeParse(deepObject(MAX_JSON_DEPTH + 1)); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues[0]?.message).toContain("maximum nesting depth"); + }); + + it.each(["object", "array"])( + "rejects the original 10000-level %s crash without throwing", + (kind) => { + let value: unknown = 1; + for (let index = 0; index < 10_000; index += 1) + value = kind === "object" ? { nested: value } : [value]; + const result = jsonValueSchema.safeParse(value); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues[0]?.message).toContain( + "maximum nesting depth", + ); + }, + ); + + it("rejects cyclic values instead of recursing forever", () => { + const cyclic: Record = {}; + cyclic.self = cyclic; + const result = jsonValueSchema.safeParse(cyclic); + expect(result.success).toBe(false); + }); + + it("keeps json semantics for non-JSON values", () => { + expect(jsonValueSchema.safeParse(undefined).success).toBe(false); + expect(jsonValueSchema.safeParse({ a: Number.NaN }).success).toBe(false); + expect( + jsonValueSchema.safeParse({ a: Number.POSITIVE_INFINITY }).success, + ).toBe(false); + expect(jsonValueSchema.safeParse({ a: 1n }).success).toBe(false); + }); + + it("accepts ordinary nested JSON", () => { + expect( + jsonValueSchema.safeParse({ + list: [1, "two", null, true, { deep: { further: [{}] } }], + }).success, + ).toBe(true); + }); +}); + +it("preserves prototype-named members without prototype mutation", () => { + const input: unknown = JSON.parse( + '{"__proto__":{"preserved":7},"constructor":"ordinary","prototype":[1],"nested":{"\\u005f\\u005fproto\\u005f\\u005f":"escaped"},"list":[{"__proto__":null}]}', + ); + const parsed = jsonObjectSchema.parse(input); + expect(parsed).toEqual(input); + expect(Object.getPrototypeOf(parsed)).toBe(Object.prototype); + expect(Object.hasOwn(parsed, "__proto__")).toBe(true); + expect(parsed["__proto__"]).toEqual({ preserved: 7 }); + expect(parsed["constructor"]).toBe("ordinary"); + expect(parsed["prototype"]).toEqual([1]); + expect(parsed["nested"]).toEqual({ ["__proto__"]: "escaped" }); + expect(parsed["list"]).toEqual([{ ["__proto__"]: null }]); + expect(Object.getPrototypeOf(parsed["nested"])).toBe(Object.prototype); + expect(Reflect.get(Object.prototype, "preserved")).toBeUndefined(); + expect(JSON.parse(JSON.stringify(parsed))).toEqual(input); +}); + +it.each([undefined, Number.NaN, Infinity, 1n, () => 0, new Date(0)])( + "rejects non-JSON values in prototype-named members (%s)", + (value) => { + for (const input of [ + { ["__proto__"]: value }, + { nested: [{ ["__proto__"]: value }] }, + ]) { + expect(jsonValueSchema.safeParse(input).success).toBe(false); + expect(jsonObjectSchema.safeParse(input).success).toBe(false); + } + }, +); diff --git a/src/domain/jsonValue.ts b/src/domain/jsonValue.ts index 710e62d612..7b6f6f649e 100644 --- a/src/domain/jsonValue.ts +++ b/src/domain/jsonValue.ts @@ -1,9 +1,87 @@ import { z } from "zod"; +/** Maximum JSON nesting accepted before recursive result cloning. */ +export const MAX_JSON_DEPTH = 1000; + /** JSON-safe value shared by domain, application, and adapter boundaries. */ -export const jsonValueSchema = z.json(); +export type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); +const recordShape = z.record(z.string(), z.unknown()); + +const jsonValidationIssue = (root: unknown): string | undefined => { + const pending: { value: unknown; depth: number }[] = [ + { value: root, depth: 0 }, + ]; + while (pending.length > 0) { + const item = pending.pop(); + if (item === undefined) break; + if (item.depth > MAX_JSON_DEPTH) + return `JSON value exceeds maximum nesting depth of ${MAX_JSON_DEPTH}`; + const value = item.value; + if ( + value === null || + typeof value === "string" || + typeof value === "boolean" + ) + continue; + if (typeof value === "number") { + if (!Number.isFinite(value)) return "JSON numbers must be finite"; + } else if (Array.isArray(value)) { + for (const child of value) + pending.push({ value: child, depth: item.depth + 1 }); + } else if (isRecord(value) && recordShape.safeParse(value).success) { + // Inspect every own member, including keys Zod's record parser skips. + for (const child of Object.values(value)) + pending.push({ value: child, depth: item.depth + 1 }); + } else return "Invalid JSON value"; + } + return undefined; +}; + +const preserveJsonValue = (value: JsonValue): JsonValue => { + if (Array.isArray(value)) return value.map(preserveJsonValue); + if (value !== null && typeof value === "object") + return Object.fromEntries( + Object.entries(value).map(([key, child]) => [ + key, + preserveJsonValue(child), + ]), + ); + return value; +}; + +const validatedJsonValue = z.custom( + (value) => jsonValidationIssue(value) === undefined, + { + error: (issue) => jsonValidationIssue(issue.input) ?? "Invalid JSON value", + }, +); +// Keep both stages nonrecursive in public and Standard Schema projections. +validatedJsonValue._zod.toJSONSchema = () => ({}); + +/** Parse bounded JSON and retain all own data members without prototype setters. */ +export const jsonValueSchema = z + .unknown() + .nonoptional() + .pipe(validatedJsonValue) + .transform(preserveJsonValue); +jsonValueSchema._zod.toJSONSchema = () => ({}); + +const objectValidation = z.custom>( + isRecord, + "Expected JSON object", +); +const objectProjection = () => ({ type: "object", additionalProperties: {} }); +objectValidation._zod.toJSONSchema = objectProjection; /** JSON object boundary for caller-visible parameter maps. */ -export const jsonObjectSchema = z.record(z.string(), jsonValueSchema); - -export type JsonValue = z.infer; +export const jsonObjectSchema = jsonValueSchema.pipe(objectValidation); +jsonObjectSchema._zod.toJSONSchema = objectProjection; diff --git a/src/domain/localPath.ts b/src/domain/localPath.ts index 43b491cf98..9248de4d71 100644 --- a/src/domain/localPath.ts +++ b/src/domain/localPath.ts @@ -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); diff --git a/src/domain/managed/managedNativeVerificationSchemas.ts b/src/domain/managed/managedNativeVerificationSchemas.ts index 03590b3894..8e543bdabe 100644 --- a/src/domain/managed/managedNativeVerificationSchemas.ts +++ b/src/domain/managed/managedNativeVerificationSchemas.ts @@ -199,7 +199,10 @@ export const managedNativeVerificationResultSchema = z path: ["summary", "native_body_unresolved"], message: "Native body summary must match unresolved implementations", }); - }); + }) + .describe( + "Counts must agree across the result: native accepted plus unsupported equals total; each P/Invoke summary count equals the number of items with that status; and native_body_unresolved equals native_implementations.unresolved. REA checks these cross-field invariants after constructing the result.", + ); export type ManagedNativeVerificationResult = z.infer< typeof managedNativeVerificationResultSchema diff --git a/src/domain/mobileApplicationInvestigation.test.ts b/src/domain/mobileApplicationInvestigation.test.ts deleted file mode 100644 index d0610263ee..0000000000 --- a/src/domain/mobileApplicationInvestigation.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - classifyMobileResource, - detectMobileFormat, - detectPlatform, - getAllArchitectures, - hasInternetPermission, - mobileArtifactManifestSchema, - nativeLibsByArchitecture, - type MobileArtifactManifest, - type AndroidManifest, - type SigningInfo, - type NativeLibrary, - type MobileResource, -} from "./mobileApplicationInvestigation.js"; - -describe("mobile application investigation", () => { - it("detects mobile format from extension", () => { - expect(detectMobileFormat("app.apk")).toBe("apk"); - expect(detectMobileFormat("app.aab")).toBe("aab"); - expect(detectMobileFormat("app.ipa")).toBe("ipa"); - expect(detectMobileFormat("classes.dex")).toBe("dex"); - expect(detectMobileFormat("lib.so")).toBe("native_lib"); - expect(detectMobileFormat("readme.txt")).toBeNull(); - }); - - it("detects platform from format", () => { - expect(detectPlatform("apk")).toBe("android"); - expect(detectPlatform("aab")).toBe("android"); - expect(detectPlatform("dex")).toBe("android"); - expect(detectPlatform("ipa")).toBe("ios"); - expect(detectPlatform("native_lib")).toBe("cross_platform"); - }); - - it("classifies resources by path", () => { - expect(classifyMobileResource("res/layout/main.xml")).toBe("layout"); - expect(classifyMobileResource("res/drawable/icon.png")).toBe("drawable"); - expect(classifyMobileResource("assets/data.json")).toBe("asset"); - expect(classifyMobileResource("AndroidManifest.xml")).toBe("manifest"); - expect(classifyMobileResource("app.entitlements")).toBe("entitlements"); - expect(classifyMobileResource("lib/arm64-v8a/libnative.so")).toBe( - "native_lib", - ); - expect(classifyMobileResource("unknown.xyz")).toBe("other"); - }); - - const androidManifest: AndroidManifest = { - package_name: "com.example.app", - version_name: "1.0.0", - version_code: 1, - min_sdk: 21, - target_sdk: 34, - permissions: ["android.permission.INTERNET", "android.permission.CAMERA"], - activities: ["MainActivity"], - services: ["BackgroundService"], - receivers: [], - providers: [], - }; - - const signing: SigningInfo = { - is_signed: true, - signer_identity: "CN=TestSigner", - certificate_fingerprint: "abc123", - signature_scheme: "v2", - is_debuggable: false, - }; - - const nativeLibs: NativeLibrary[] = [ - { - path: "lib/arm64-v8a/libnative.so", - architecture: "arm64-v8a", - size: 100000, - digest: "a".repeat(64), - }, - { - path: "lib/armeabi-v7a/libnative.so", - architecture: "armeabi-v7a", - size: 90000, - digest: "b".repeat(64), - }, - ]; - - const resources: MobileResource[] = [ - { path: "res/layout/main.xml", type: "layout", size: 1000 }, - { path: "res/drawable/icon.png", type: "drawable", size: 5000 }, - ]; - - const validManifest: MobileArtifactManifest = { - format: "apk", - platform: "android", - app_name: "TestApp", - android_manifest: androidManifest, - ios_manifest: null, - signing, - resources, - native_libraries: nativeLibs, - total_size: 5000000, - architectures: ["arm64-v8a", "armeabi-v7a"], - }; - - it("rejects a manifest with a negative total size", () => { - const result = mobileArtifactManifestSchema.safeParse({ - ...validManifest, - total_size: -1, - }); - expect(result.success).toBe(false); - }); - - it("filters native libs by architecture", () => { - const arm64 = nativeLibsByArchitecture(validManifest, "arm64-v8a"); - expect(arm64).toHaveLength(1); - expect(arm64[0]!.architecture).toBe("arm64-v8a"); - }); - - it("checks internet permission", () => { - expect(hasInternetPermission(validManifest)).toBe(true); - const noInternet: MobileArtifactManifest = { - ...validManifest, - android_manifest: { ...androidManifest, permissions: [] }, - }; - expect(hasInternetPermission(noInternet)).toBe(false); - }); - - it("gets all architectures", () => { - const archs = getAllArchitectures(validManifest); - expect(archs).toHaveLength(2); - expect(archs).toContain("arm64-v8a"); - expect(archs).toContain("armeabi-v7a"); - }); -}); diff --git a/src/domain/mobileApplicationInvestigation.ts b/src/domain/mobileApplicationInvestigation.ts deleted file mode 100644 index 95a4b531f1..0000000000 --- a/src/domain/mobileApplicationInvestigation.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { z } from "zod"; - -/** Supported mobile artifact formats. */ -export const mobileArtifactFormatSchema = z.enum([ - "apk", - "aab", - "ipa", - "dex", - "native_lib", -]); -export type MobileArtifactFormat = z.infer; - -/** Mobile platform. */ -export const mobilePlatformSchema = z.enum([ - "android", - "ios", - "cross_platform", -]); -export type MobilePlatform = z.infer; - -/** Android manifest entry. */ -export const androidManifestSchema = z.strictObject({ - package_name: z.string().min(1), - version_name: z.string().nullable(), - version_code: z.number().int().nullable(), - min_sdk: z.number().int().nullable(), - target_sdk: z.number().int().nullable(), - permissions: z.array(z.string()).default([]), - activities: z.array(z.string()).default([]), - services: z.array(z.string()).default([]), - receivers: z.array(z.string()).default([]), - providers: z.array(z.string()).default([]), -}); -export type AndroidManifest = z.infer; - -/** iOS app manifest (Info.plist). */ -export const iosManifestSchema = z.strictObject({ - bundle_identifier: z.string().min(1), - bundle_version: z.string().nullable(), - short_version: z.string().nullable(), - minimum_os_version: z.string().nullable(), - supported_platforms: z.array(z.string()).default([]), - ui_required_device_capabilities: z.array(z.string()).default([]), - app_permissions: z.array(z.string()).default([]), -}); -/** Signing information. */ -export const signingInfoSchema = z.strictObject({ - is_signed: z.boolean().default(false), - signer_identity: z.string().nullable(), - certificate_fingerprint: z.string().nullable(), - signature_scheme: z.string().nullable(), - is_debuggable: z.boolean().default(false), -}); -export type SigningInfo = z.infer; - -/** Native library entry. */ -export const nativeLibrarySchema = z.strictObject({ - path: z.string().min(1), - architecture: z.string().nullable(), - size: z.number().int().nonnegative(), - digest: z - .string() - .regex(/^[a-f0-9]{64}$/u) - .nullable(), -}); -export type NativeLibrary = z.infer; - -/** Resource entry within a mobile artifact. */ -export const mobileResourceSchema = z.strictObject({ - path: z.string().min(1), - type: z.enum([ - "layout", - "drawable", - "string", - "asset", - "raw", - "native_lib", - "manifest", - "entitlements", - "provisioning", - "other", - ]), - size: z.number().int().nonnegative(), -}); -export type MobileResource = z.infer; - -/** Static analysis result for a mobile artifact. */ -export const mobileArtifactManifestSchema = z.strictObject({ - format: mobileArtifactFormatSchema, - platform: mobilePlatformSchema, - app_name: z.string().nullable(), - android_manifest: androidManifestSchema.nullable(), - ios_manifest: iosManifestSchema.nullable(), - signing: signingInfoSchema, - resources: z.array(mobileResourceSchema).default([]), - native_libraries: z.array(nativeLibrarySchema).default([]), - total_size: z.number().int().nonnegative(), - architectures: z.array(z.string()).default([]), -}); -export type MobileArtifactManifest = z.infer< - typeof mobileArtifactManifestSchema ->; - -/** Detect mobile artifact format from file extension. */ -export function detectMobileFormat(path: string): MobileArtifactFormat | null { - const lower = path.toLowerCase(); - if (lower.endsWith(".apk")) return "apk"; - if (lower.endsWith(".aab")) return "aab"; - if (lower.endsWith(".ipa")) return "ipa"; - if (lower.endsWith(".dex")) return "dex"; - if (lower.endsWith(".so") || lower.endsWith(".dylib") || lower.endsWith(".a")) - return "native_lib"; - return null; -} - -/** Detect platform from format. */ -export function detectPlatform(format: MobileArtifactFormat): MobilePlatform { - switch (format) { - case "apk": - case "aab": - case "dex": - return "android"; - case "ipa": - return "ios"; - case "native_lib": - return "cross_platform"; - } -} - -/** Classify a resource by its path. */ -export function classifyMobileResource(path: string): MobileResource["type"] { - const lower = path.toLowerCase(); - if (lower.includes("androidmanifest.xml")) return "manifest"; - if (lower.includes(".entitlements")) return "entitlements"; - if (lower.includes("provisioning") || lower.includes(".mobileprovision")) - return "provisioning"; - if (lower.includes("/layout/")) return "layout"; - if (lower.includes("/drawable/") || lower.includes("/mipmap/")) - return "drawable"; - if (lower.includes("/values/")) return "string"; - if (lower.includes("assets/") || lower.includes("raw/")) return "asset"; - if (lower.endsWith(".so") || lower.endsWith(".dylib")) return "native_lib"; - if (lower.endsWith(".xml")) return "layout"; - return "other"; -} - -/** Get all native libraries for a specific architecture. */ -export function nativeLibsByArchitecture( - manifest: MobileArtifactManifest, - arch: string, -): NativeLibrary[] { - return manifest.native_libraries.filter((l) => l.architecture === arch); -} - -/** Check if an Android artifact has internet permission. */ -export function hasInternetPermission( - manifest: MobileArtifactManifest, -): boolean { - if (!manifest.android_manifest) return false; - return manifest.android_manifest.permissions.some( - (p) => p.includes("INTERNET") || p.includes("android.permission.INTERNET"), - ); -} - -/** Get all unique architectures across native libraries. */ -export function getAllArchitectures( - manifest: MobileArtifactManifest, -): string[] { - const archs = new Set(); - for (const lib of manifest.native_libraries) { - if (lib.architecture) archs.add(lib.architecture); - } - for (const arch of manifest.architectures) { - archs.add(arch); - } - return [...archs]; -} diff --git a/src/domain/native/binaryLayout.test.ts b/src/domain/native/binaryLayout.test.ts new file mode 100644 index 0000000000..9f8f970e0e --- /dev/null +++ b/src/domain/native/binaryLayout.test.ts @@ -0,0 +1,362 @@ +import { expect, it } from "vitest"; +import { binaryLayoutSchema, type BinaryLayout } from "./binaryLayout.js"; + +const report = () => ({ + artifact: { path: "/fixture.elf", sha256: "a".repeat(64), bytes: 128 }, + format: "elf", + architecture: { machine: "EM_X86_64", bits: 64, byte_order: "little" }, + image_type: "ET_EXEC", + entry_point: { + reported_value: "0x20000000000001", + meaning: "linked-virtual-address", + execution_status: "unknown", + }, + runtime_load_base: null, + sections: [ + { + index: 0, + name: { + display: "opaque", + bytes_base64: "b3BhcXVl", + location: { offset: "0x40", bytes: "0x7" }, + unknown_reason: null, + }, + name_offset: "0x0", + type: "SHT_NOBITS", + header_location: { offset: "0x0", bytes: "0x40" }, + address: "0x20000000000001", + offset: "0xffffffffffffffff", + size: "0x2000", + alignment: "0x1", + flags: "0x3", + link: 0, + info: 0, + entry_size: "0x0", + file_backing: "none", + }, + ], + segments: [], + symbols: [], + relocations: [], + packed_relative_relocations: [], + relocation_inventory_completeness: "unknown", + linkage: { + needed_libraries: [], + interpreters: [], + got: [], + plt: [], + convenience_maps_completeness: "unknown", + runtime_library_paths: null, + }, + mitigations: { + evidence_kind: "inferred", + position_independent: false, + nx_indicator: null, + executable_stack_indicator: false, + stack_canary_indicator: false, + relro: null, + }, + diagnostics: { stdout: "", stderr: "", truncated: false }, + limitations: [], +}); + +it("retains high linked values and NOBITS without inventing file backing", () => { + const value = binaryLayoutSchema.parse(report()); + expect(value.entry_point.reported_value).toBe("0x20000000000001"); + expect(value.sections[0]?.offset).toBe("0xffffffffffffffff"); + expect(value.runtime_load_base).toBeNull(); +}); + +it.each([ + "file-backed-outside", + "header-outside", + "wrong-index", + "noncanonical-base64", + "wrong-machine", + "outside-elf64", +])("rejects a malformed source representation: %s", (problem) => { + const value = report(); + const section = value.sections[0]; + if (section === undefined) throw new Error("section missing"); + if (problem === "file-backed-outside") section.file_backing = "file"; + if (problem === "header-outside") section.header_location.offset = "0x80"; + if (problem === "wrong-index") section.index = 1; + if (problem === "wrong-machine") value.architecture.machine = "EM_AARCH64"; + if (problem === "outside-elf64") + value.entry_point.reported_value = "0x10000000000000000"; + if (problem === "noncanonical-base64") section.name.bytes_base64 = "AR=="; + expect(binaryLayoutSchema.safeParse(value).success).toBe(false); +}); + +it.each([ + ["PT_NULL", "0xffffffffffffffff", "none", null, true], + ["PT_LOAD", "0x0", "none", { read: true, write: true, execute: false }, true], + [ + "PT_LOAD", + "0x1", + "file", + { read: true, write: false, execute: false }, + false, + ], + ["PT_NULL", "0x1", "file", null, false], + [ + "PT_NULL", + "0x1", + "none", + { read: false, write: false, execute: false }, + false, + ], + [ + "PT_LOAD", + "0x1", + "none", + { read: true, write: false, execute: false }, + false, + ], +])( + "validates actual segment backing/meaning for %s size %s", + (type, fileSize, backing, permissions, valid) => { + const value = { + ...report(), + segments: [ + { + index: 0, + type, + header_location: { offset: "0x0", bytes: "0x38" }, + offset: "0xffffffffffffffff", + file_size: fileSize, + memory_size: fileSize, + virtual_address: "0x0", + physical_address: "0x0", + alignment: "0x0", + flags: "0x0", + file_backing: backing, + permissions, + }, + ], + }; + expect(binaryLayoutSchema.safeParse(value).success).toBe(valid); + }, +); + +it.each([ + ["ET_EXEC", "0x0", "absent", true], + ["ET_DYN", "0x0", "linked-virtual-address", false], + ["ET_REL", "0x0", "not-applicable", true], + ["ET_EXEC", "0x1", "absent", false], +])( + "validates actual entry meaning: %s %s %s", + (imageType, entry, meaning, valid) => { + expect( + binaryLayoutSchema.safeParse({ + ...report(), + image_type: imageType, + entry_point: { + reported_value: entry, + meaning, + execution_status: "unknown", + }, + }).success, + ).toBe(valid); + }, +); + +it.each([ + "valid", + "byte-length", + "section-identity", + "decoded-order", + "noncanonical-bytes", +])( + "retains original packed bytes and derived offset provenance: %s", + (problem) => { + const value = report(); + const section = value.sections[0]; + if (section === undefined) throw new Error("section missing"); + section.type = "SHT_RELR"; + section.file_backing = "file"; + section.offset = "0x60"; + section.size = "0x8"; + const table = { + section_index: 0, + location: { offset: "0x60", bytes: "0x8" }, + encoded_bytes_base64: "AAAAAAAAAAA=", + entries: [{ decoded_index: 0, reported_offset: "0x20000000000001" }], + offset_meaning: "linked-virtual-address", + evidence_kind: "derived", + entry_source_locations: null, + addends: null, + }; + if (problem === "byte-length") table.location.bytes = "0x7"; + if (problem === "section-identity") table.section_index = 1; + if (problem === "decoded-order") + table.entries[0] = { decoded_index: 1, reported_offset: "0x0" }; + if (problem === "noncanonical-bytes") table.encoded_bytes_base64 = "AB=="; + expect( + binaryLayoutSchema.safeParse({ + ...value, + packed_relative_relocations: [table], + }).success, + ).toBe(problem === "valid"); + }, +); + +it.each([ + "valid", + "zero-symbol", + "zero-without-table", + "missing-table", + "wrong-table-kind", + "missing-symbol", + "symbol-outside-table", + "positive-without-table", + "owner-link-mismatch", + "wrong-meaning", + "valid-section-target", + "missing-target", + "wrong-target-owner", + "wrong-target-offset", + "undefined-target", + "inactive-target", + "false-resolved-zero-target", + "wrong-unknown-target", + "missing-string-table", + "wrong-string-table-kind", +])("validates relocation symbol reference semantics: %s", (problem) => { + const value = binaryLayoutSchema.parse(report()); + value.artifact.bytes = 512; + const base = value.sections[0]; + if (base === undefined) throw new Error("section missing"); + value.sections.push( + { + ...base, + index: 1, + type: "SHT_SYMTAB", + link: 3, + header_location: { offset: "0x40", bytes: "0x40" }, + offset: "0x100", + size: "0x30", + entry_size: "0x18", + file_backing: "file", + }, + { + ...base, + index: 2, + type: "SHT_RELA", + header_location: { offset: "0x80", bytes: "0x40" }, + offset: "0x130", + size: "0x18", + entry_size: "0x18", + link: 1, + file_backing: "file", + }, + { + ...base, + index: 3, + type: "SHT_STRTAB", + offset: "0x40", + size: "0x7", + file_backing: "file", + }, + ); + value.symbols.push({ + table_index: 1, + entry_index: 1, + name: base.name, + name_offset: "0x0", + location: { offset: "0x118", bytes: "0x18" }, + value: "0x0", + value_meaning: "undefined", + size: "0x0", + binding: "STB_GLOBAL", + type: "STT_NOTYPE", + visibility: "STV_DEFAULT", + section_index: "SHN_UNDEF", + }); + const relocation: BinaryLayout["relocations"][number] = { + section_index: 2, + entry_index: 0, + reported_offset: "0x0", + target: { kind: "linked-virtual-address", address: "0x0" }, + location: { offset: "0x130", bytes: "0x18" }, + type: 1, + symbol_table_index: 1, + symbol_index: 1, + symbol_reference_meaning: "symbol-table-entry", + addend: "0", + }; + const table = value.sections[1]; + const owner = value.sections[2]; + if (table === undefined || owner === undefined) + throw new Error("table or relocation owner missing"); + if (problem.startsWith("zero")) { + relocation.symbol_index = 0; + relocation.symbol_reference_meaning = "zero-symbol-value"; + } + if (problem === "zero-without-table" || problem === "positive-without-table") + owner.link = relocation.symbol_table_index = 0; + if (problem === "missing-table") + owner.link = relocation.symbol_table_index = 99; + if (problem === "wrong-table-kind") table.type = "SHT_PROGBITS"; + if (problem === "missing-string-table") table.link = 99; + if (problem === "wrong-string-table-kind") table.link = 0; + if (problem === "missing-symbol") value.symbols = []; + if (problem === "symbol-outside-table") table.size = "0x18"; + if (problem === "owner-link-mismatch") owner.link = 0; + if (problem === "wrong-meaning") + relocation.symbol_reference_meaning = "zero-symbol-value"; + if (problem.includes("target")) { + value.image_type = "ET_REL"; + value.entry_point.meaning = "not-applicable"; + owner.info = 1; + relocation.target = { + kind: "section-offset", + section_index: + problem === "missing-target" + ? 99 + : problem === "wrong-target-owner" + ? 2 + : 1, + offset: problem === "wrong-target-offset" ? "0x1" : "0x0", + }; + if (problem === "missing-target") owner.info = 99; + if (problem === "false-resolved-zero-target") { + owner.info = 0; + relocation.target.section_index = 0; + } + if ( + ["undefined-target", "inactive-target", "wrong-unknown-target"].includes( + problem, + ) + ) { + if (problem === "inactive-target") + value.sections.push({ ...base, index: 4, type: "SHT_NULL" }); + owner.info = + problem === "undefined-target" + ? 0 + : problem === "inactive-target" + ? 4 + : 1; + relocation.target = { + kind: "unknown-section", + reported_section_index: owner.info, + offset: "0x0", + unknown_reason: + problem === "undefined-target" + ? "undefined-section-reference" + : "inactive-section-header", + }; + } + } + value.relocations.push(relocation); + expect(binaryLayoutSchema.safeParse(value).success).toBe( + [ + "valid", + "zero-symbol", + "zero-without-table", + "valid-section-target", + "undefined-target", + "inactive-target", + ].includes(problem), + ); +}); diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts new file mode 100644 index 0000000000..95a55b8669 --- /dev/null +++ b/src/domain/native/binaryLayout.ts @@ -0,0 +1,443 @@ +import { z } from "zod"; + +const unsignedHex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/); +const index = z.number().int().nonnegative(); +const scalar = z.union([z.string(), z.number().int()]); +const range = z.strictObject({ offset: unsignedHex, bytes: unsignedHex }); +const canonicalBase64 = z + .string() + .regex( + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$/, + ); +const name = z.strictObject({ + display: z.string(), + bytes_base64: canonicalBase64.nullable(), + location: range.nullable(), + unknown_reason: z.string().nullable(), +}); + +/** Explicit local object selection, independent of an active disassembler target. */ +export const inspectBinaryLayoutInputSchema = z.strictObject({ + path: z + .string() + .min(1) + .describe("Absolute filesystem path to the selected binary"), +}); + +/** File and linked-image evidence; numeric ELF values never pass through unsafe JSON numbers. */ +const binaryLayoutObjectSchema = z.strictObject({ + artifact: z.strictObject({ + path: z.string().min(1), + sha256: z.string().regex(/^[a-f0-9]{64}$/), + bytes: index, + }), + format: z.literal("elf"), + architecture: z.strictObject({ + machine: z.literal("EM_X86_64"), + bits: z.literal(64), + byte_order: z.literal("little"), + }), + image_type: z.enum(["ET_EXEC", "ET_DYN", "ET_REL"]), + entry_point: z.strictObject({ + reported_value: unsignedHex, + meaning: z.enum(["linked-virtual-address", "not-applicable", "absent"]), + execution_status: z.literal("unknown"), + }), + runtime_load_base: z.null(), + sections: z.array( + z.strictObject({ + index, + name, + name_offset: unsignedHex, + type: scalar, + header_location: range, + address: unsignedHex, + offset: unsignedHex, + size: unsignedHex, + alignment: unsignedHex, + flags: unsignedHex, + link: index, + info: index, + entry_size: unsignedHex, + file_backing: z.enum(["file", "none"]), + }), + ), + segments: z.array( + z.strictObject({ + index, + type: scalar, + header_location: range, + offset: unsignedHex, + file_size: unsignedHex, + memory_size: unsignedHex, + virtual_address: unsignedHex, + physical_address: unsignedHex, + alignment: unsignedHex, + flags: unsignedHex, + file_backing: z.enum(["file", "none"]), + permissions: z + .strictObject({ + read: z.boolean(), + write: z.boolean(), + execute: z.boolean(), + }) + .nullable(), + }), + ), + symbols: z.array( + z.strictObject({ + table_index: index, + entry_index: index, + name, + name_offset: unsignedHex, + location: range, + value: unsignedHex, + value_meaning: z.enum([ + "undefined", + "alignment", + "absolute-value", + "no-address", + "unknown-section-index", + "section-offset", + "tls-offset", + "linked-virtual-address", + ]), + size: unsignedHex, + binding: scalar, + type: scalar, + visibility: scalar, + section_index: scalar, + }), + ), + relocations: z.array( + z.strictObject({ + section_index: index, + entry_index: index, + reported_offset: unsignedHex, + target: z.discriminatedUnion("kind", [ + z.strictObject({ + kind: z.literal("section-offset"), + section_index: index, + offset: unsignedHex, + }), + z.strictObject({ + kind: z.literal("linked-virtual-address"), + address: unsignedHex, + }), + z.strictObject({ + kind: z.literal("unknown-section"), + reported_section_index: index, + offset: unsignedHex, + unknown_reason: z.enum([ + "undefined-section-reference", + "inactive-section-header", + ]), + }), + ]), + location: range, + type: index, + symbol_table_index: index, + symbol_index: index, + symbol_reference_meaning: z.enum([ + "zero-symbol-value", + "symbol-table-entry", + ]), + addend: z + .string() + .regex(/^-?(?:0|[1-9][0-9]{0,18})$/) + .nullable(), + }), + ), + packed_relative_relocations: z.array( + z.strictObject({ + section_index: index, + location: range, + encoded_bytes_base64: canonicalBase64, + entries: z.array( + z.strictObject({ decoded_index: index, reported_offset: unsignedHex }), + ), + offset_meaning: z.enum(["linked-virtual-address", "unknown"]), + evidence_kind: z.literal("derived"), + entry_source_locations: z.null(), + addends: z.null(), + }), + ), + relocation_inventory_completeness: z.literal("unknown"), + linkage: z.strictObject({ + needed_libraries: z.array(name), + interpreters: z.array(name), + got: z.array( + z.strictObject({ display_name: z.string(), address: unsignedHex }), + ), + plt: z.array( + z.strictObject({ display_name: z.string(), address: unsignedHex }), + ), + convenience_maps_completeness: z.literal("unknown"), + runtime_library_paths: z.null(), + }), + mitigations: z.strictObject({ + evidence_kind: z.literal("inferred"), + position_independent: z.boolean(), + nx_indicator: z.boolean().nullable(), + executable_stack_indicator: z.boolean(), + stack_canary_indicator: z.boolean(), + relro: z.enum(["Partial", "Full"]).nullable(), + }), + diagnostics: z.strictObject({ + stdout: z.string(), + stderr: z.string(), + truncated: z.boolean(), + }), + limitations: z.array(z.string()), +}); + +/** Decoder payload excludes the artifact identity observed by the owning process boundary. */ +export const binaryLayoutPayloadSchema = binaryLayoutObjectSchema.omit({ + artifact: true, + diagnostics: true, +}); + +/** Check source ranges again at the public boundary, independently of upstream parsing. */ +export const binaryLayoutSchema = binaryLayoutObjectSchema.superRefine( + (value, context) => { + const size = BigInt(value.artifact.bytes); + const check = ( + offset: string, + length: string, + path: (string | number)[], + ): void => { + const start = BigInt(offset); + const bytes = BigInt(length); + if (start > size || bytes > size - start) + context.addIssue({ + code: "custom", + path, + message: + "Reported file-backed range lies outside the selected artifact.", + }); + }; + const checkLocation = ( + where: z.infer, + path: (string | number)[], + ): void => check(where.offset, where.bytes, path); + const checkName = ( + reported: z.infer, + path: (string | number)[], + ): void => { + if (reported.location !== null) + checkLocation(reported.location, [...path, "location"]); + }; + const expectedEntryMeaning = + value.image_type === "ET_REL" + ? "not-applicable" + : value.entry_point.reported_value === "0x0" + ? "absent" + : "linked-virtual-address"; + if (value.entry_point.meaning !== expectedEntryMeaning) + context.addIssue({ + code: "custom", + path: ["entry_point", "meaning"], + message: + "Entry meaning must distinguish zero/unused fields from an actual linked address.", + }); + for (const [index, section] of value.sections.entries()) { + if ( + (section.type === "SHT_SYMTAB" || section.type === "SHT_DYNSYM") && + value.sections[section.link]?.type !== "SHT_STRTAB" + ) + context.addIssue({ + code: "custom", + path: ["sections", index, "link"], + message: + "Symbol tables must link to an existing string-table section.", + }); + if (section.index !== index) + context.addIssue({ + code: "custom", + path: ["sections", index, "index"], + message: + "Complete section order must preserve original producer indices.", + }); + checkLocation(section.header_location, [ + "sections", + index, + "header_location", + ]); + checkName(section.name, ["sections", index, "name"]); + if (section.file_backing === "file") + check(section.offset, section.size, ["sections", index]); + } + for (const [index, segment] of value.segments.entries()) { + if (segment.index !== index) + context.addIssue({ + code: "custom", + path: ["segments", index, "index"], + message: + "Complete segment order must preserve original producer indices.", + }); + checkLocation(segment.header_location, [ + "segments", + index, + "header_location", + ]); + const hasFileBytes = + segment.type !== "PT_NULL" && BigInt(segment.file_size) !== 0n; + if (segment.file_backing !== (hasFileBytes ? "file" : "none")) + context.addIssue({ + code: "custom", + path: ["segments", index, "file_backing"], + message: + "File backing must follow the reported segment type and file size.", + }); + if ((segment.type === "PT_NULL") !== (segment.permissions === null)) + context.addIssue({ + code: "custom", + path: ["segments", index, "permissions"], + message: + "Unused PT_NULL flags have no permission meaning; other segment flags retain their interpretation.", + }); + if (hasFileBytes) + check(segment.offset, segment.file_size, ["segments", index]); + } + for (const [index, symbol] of value.symbols.entries()) { + checkLocation(symbol.location, ["symbols", index, "location"]); + checkName(symbol.name, ["symbols", index, "name"]); + } + for (const [index, relocation] of value.relocations.entries()) + checkLocation(relocation.location, ["relocations", index, "location"]); + validateRelocationReferences(value, context); + validatePackedRelativeTables(value, context, checkLocation); + for (const facet of ["needed_libraries", "interpreters"] as const) + for (const [index, reported] of value.linkage[facet].entries()) + checkName(reported, ["linkage", facet, index]); + }, +); + +export type InspectBinaryLayoutInput = z.infer< + typeof inspectBinaryLayoutInputSchema +>; +export type BinaryLayout = z.infer; + +const validateRelocationReferences = ( + value: z.output, + context: z.RefinementCtx, +): void => { + const symbols = new Set( + value.symbols.map( + (symbol) => `${symbol.table_index}:${symbol.entry_index}`, + ), + ); + for (const [index, relocation] of value.relocations.entries()) { + const owner = value.sections[relocation.section_index]; + const target = relocation.target; + const targetMatches = + value.image_type === "ET_REL" + ? target.kind === "section-offset" + ? target.section_index !== 0 && + target.section_index === owner?.info && + value.sections[target.section_index] !== undefined && + value.sections[target.section_index]?.type !== "SHT_NULL" && + target.offset === relocation.reported_offset + : target.kind === "unknown-section" && + target.reported_section_index === owner?.info && + target.offset === relocation.reported_offset && + (target.reported_section_index === 0 + ? target.unknown_reason === "undefined-section-reference" + : value.sections[target.reported_section_index]?.type === + "SHT_NULL" && + target.unknown_reason === "inactive-section-header") + : target.kind === "linked-virtual-address" && + target.address === relocation.reported_offset; + if (!targetMatches) + context.addIssue({ + code: "custom", + path: ["relocations", index, "target"], + message: + "Relocation target must preserve its reported offset and owner reference; SHN_UNDEF and inactive headers remain unknown rather than resolved sections.", + }); + const table = value.sections[relocation.symbol_table_index]; + const tableExists = + relocation.symbol_table_index !== 0 && + (table?.type === "SHT_SYMTAB" || table?.type === "SHT_DYNSYM"); + const positiveReferenceExists = + tableExists && + BigInt(table.entry_size) >= 24n && + BigInt(relocation.symbol_index) < + BigInt(table.size) / BigInt(table.entry_size) && + symbols.has( + `${relocation.symbol_table_index}:${relocation.symbol_index}`, + ); + if ( + (owner?.type !== "SHT_REL" && owner?.type !== "SHT_RELA") || + owner.link !== relocation.symbol_table_index || + (relocation.symbol_table_index !== 0 && !tableExists) || + (relocation.symbol_index !== 0 && !positiveReferenceExists) || + relocation.symbol_reference_meaning !== + (relocation.symbol_index === 0 + ? "zero-symbol-value" + : "symbol-table-entry") + ) + context.addIssue({ + code: "custom", + path: ["relocations", index], + message: + "Relocation must preserve its REL/RELA owner and resolve positive symbol indices through the linked symbol table; index zero uses a zero symbol value.", + }); + } +}; + +const validatePackedRelativeTables = ( + value: z.output, + context: z.RefinementCtx, + checkLocation: ( + location: z.output, + path: (string | number)[], + ) => void, +): void => { + for (const [ + tableIndex, + table, + ] of value.packed_relative_relocations.entries()) { + checkLocation(table.location, [ + "packed_relative_relocations", + tableIndex, + "location", + ]); + const encodedBytes = + (table.encoded_bytes_base64.length * 3) / 4 - + (table.encoded_bytes_base64.endsWith("==") + ? 2 + : table.encoded_bytes_base64.endsWith("=") + ? 1 + : 0); + if ( + !Number.isInteger(encodedBytes) || + BigInt(encodedBytes) !== BigInt(table.location.bytes) + ) + context.addIssue({ + code: "custom", + path: ["packed_relative_relocations", tableIndex], + message: + "Complete packed table bytes must match its original file range.", + }); + const section = value.sections[table.section_index]; + if ( + section?.type !== "SHT_RELR" || + section.offset !== table.location.offset || + section.size !== table.location.bytes + ) + context.addIssue({ + code: "custom", + path: ["packed_relative_relocations", tableIndex, "section_index"], + message: + "Packed relative evidence must retain its original RELR section identity and range.", + }); + for (const [decoded, entry] of table.entries.entries()) + if (entry.decoded_index !== decoded) + context.addIssue({ + code: "custom", + path: ["packed_relative_relocations", tableIndex, "entries", decoded], + message: "Decoded offsets must preserve upstream iterator order.", + }); + } +}; diff --git a/src/domain/native/nativeCallObservation.ts b/src/domain/native/nativeCallObservation.ts new file mode 100644 index 0000000000..ecea77d774 --- /dev/null +++ b/src/domain/native/nativeCallObservation.ts @@ -0,0 +1,198 @@ +import { z } from "zod"; + +const hexSchema = z.string().regex(/^0x[0-9a-f]+$/u); +/** Objective-C class names and selectors never contain spaces or brackets. */ +const objcName = z + .string() + .min(1) + .regex(/^[^\s[\]]+$/u, "Use the bare name without spaces or brackets"); + +const functionBreakpoint = { + kind: z.literal("function"), + name: z + .string() + .min(1) + .describe( + "Exact symbol name, such as `open`, a C++ or mangled Swift symbol, or `-[NSString length]`.", + ), +}; + +const objcMethodBreakpoint = { + kind: z.literal("objc-method"), + selector: objcName.describe( + "Selector such as `dataTaskWithRequest:completionHandler:`.", + ), +}; + +/** One function or Objective-C method whose entries are observed. */ +export const nativeCallBreakpointSchema = z.discriminatedUnion("kind", [ + z.strictObject({ + ...functionBreakpoint, + module: z + .string() + .min(1) + .optional() + .describe( + "Image file name that defines the symbol, such as `libsystem_kernel.dylib`; omit to match every image.", + ), + }), + z.strictObject({ + ...objcMethodBreakpoint, + class_name: objcName + .optional() + .describe("Implementing class; omit to match every class."), + method_type: z.enum(["instance", "class", "any"]).default("any"), + }), +]); + +/** + * Launch the active Mach-O as an owned process under LLDB and observe calls. + * Duration and event bounds keep runtime observation finite. + */ +export const nativeCallObservationInputSchema = z.strictObject({ + breakpoints: z.array(nativeCallBreakpointSchema).min(1), + arguments: z.array(z.string()).default([]), + environment: z + .record( + z + .string() + .min(1) + .regex(/^[^=\0]+$/u, "Environment names cannot contain '=' or NUL"), + z.string().regex(/^[^\0]*$/u, "Environment values cannot contain NUL"), + ) + .default({}) + .describe("Overrides on top of the environment REA runs with."), + working_directory: z.string().min(1).optional(), + duration_ms: z + .number() + .int() + .min(100) + .max(600_000) + .default(10_000) + .describe( + "Observation window; the process is killed if it is still running.", + ), + max_events: z + .number() + .int() + .min(1) + .max(100_000) + .default(1_000) + .describe( + "Stop observing and kill the process after this many recorded entries.", + ), + argument_registers: z + .number() + .int() + .min(0) + .max(8) + .default(4) + .describe( + "Integer argument registers to record per entry (arm64 has 8, x86_64 has 6).", + ), + backtrace_frames: z + .number() + .int() + .min(0) + .max(64) + .default(0) + .describe("Caller frames to record per entry."), +}); + +export type NativeCallObservationInput = z.infer< + typeof nativeCallObservationInputSchema +>; + +/** A code address with the image and symbol LLDB attributes to it. */ +export const nativeCodeLocationSchema = z.strictObject({ + load_address: hexSchema, + file_address: hexSchema.nullable(), + module: z.string().nullable(), + module_path: z.string().nullable(), + symbol: z.string().nullable(), +}); + +/** One observed entry into a requested function or method. */ +export const nativeCallEventSchema = z.strictObject({ + sequence: z.number().int().nonnegative(), + elapsed_ms: z.number().nonnegative(), + thread_id: z.number().int().nonnegative(), + breakpoint_index: z.number().int().nonnegative(), + ...nativeCodeLocationSchema.shape, + /** Dynamic class of an instance-method receiver, read without running code. */ + receiver_class: z.string().nullable(), + /** Selector string read from the `_cmd` register of an Objective-C method. */ + selector: z.string().nullable(), + registers: z.array(z.strictObject({ name: z.string(), value: hexSchema })), + backtrace: z.array(nativeCodeLocationSchema), +}); + +const capturedOutputSchema = z.strictObject({ + text: z.string(), + bytes: z.number().int().nonnegative(), + truncated: z.boolean(), +}); + +/** Observed calls, the process lifecycle, and breakpoint resolution. */ +export const nativeCallObservationResultSchema = z.strictObject({ + target: z.strictObject({ + path: z.string(), + sha256: z.string().regex(/^[0-9a-f]{64}$/u), + architecture: z.string(), + arguments: z.array(z.string()), + working_directory: z.string().nullable(), + }), + debugger: z.strictObject({ + path: z.string(), + sha256: z.string().regex(/^[0-9a-f]{64}$/u), + version: z.string().nullable(), + }), + process: z.strictObject({ + pid: z.number().int().positive(), + outcome: z.enum([ + "exited", + "duration-elapsed", + "event-limit", + "stop-limit", + ]), + exit_status: z.number().int().nullable(), + exit_description: z.string().nullable(), + /** REA confirmed the process is gone when observation ended. */ + terminated: z.boolean(), + elapsed_ms: z.number().nonnegative(), + stdout: capturedOutputSchema, + stderr: capturedOutputSchema, + /** Signal and exception stops LLDB reported; the process continued past each. */ + other_stops: z.array(z.string()), + }), + breakpoints: z.array( + z.strictObject({ + index: z.number().int().nonnegative(), + request: z.discriminatedUnion("kind", [ + z.strictObject({ + ...functionBreakpoint, + module: z.string().nullable(), + }), + z.strictObject({ + ...objcMethodBreakpoint, + class_name: z.string().nullable(), + method_type: z.enum(["instance", "class", "any"]), + }), + ]), + location_count: z.number().int().nonnegative(), + locations: z.array(nativeCodeLocationSchema), + }), + ), + events: z.array(nativeCallEventSchema), + coverage: z.strictObject({ + status: z.enum(["complete", "partial"]), + event_limit_reached: z.boolean(), + /** Requests that matched no code in any image loaded while observing. */ + unresolved_breakpoints: z.array(z.number().int().nonnegative()), + }), + limitations: z.array(z.string()), +}); + +export type NativeCallObservationResult = z.infer< + typeof nativeCallObservationResultSchema +>; diff --git a/src/domain/native/nativeUiObservation.ts b/src/domain/native/nativeUiObservation.ts index 9c56488f44..a9378ad7b2 100644 --- a/src/domain/native/nativeUiObservation.ts +++ b/src/domain/native/nativeUiObservation.ts @@ -60,10 +60,26 @@ export const nativeUiScenarioInputSchema = z.strictObject({ const nativeUiScreenshotSchema = z .strictObject({ mime_type: z.literal("image/png"), - base64: z.string().min(4), - sha256: z.string().regex(/^[a-f0-9]{64}$/u), - width: z.number().int().positive(), - height: z.number().int().positive(), + base64: z + .string() + .min(4) + .describe( + "Canonical base64 encoding of PNG bytes; the decoded bytes must have a PNG signature and IHDR chunk.", + ), + sha256: z + .string() + .regex(/^[a-f0-9]{64}$/u) + .describe("SHA-256 digest of the decoded PNG bytes."), + width: z + .number() + .int() + .positive() + .describe("Must equal the width encoded in the PNG IHDR chunk."), + height: z + .number() + .int() + .positive() + .describe("Must equal the height encoded in the PNG IHDR chunk."), }) .superRefine((screenshot, context) => { const bytes = decodeCanonicalBase64(screenshot.base64); @@ -117,7 +133,7 @@ export const nativeUiSnapshotSchema = z.strictObject({ title: z.string().nullable(), value: z.string().nullable(), actions: z.array(z.string()), - children_count: z.number().int().nonnegative(), + children_count: z.number().int().nonnegative().nullable(), }), ), truncated: z.boolean(), diff --git a/src/domain/native/objcSwiftMetadata.test.ts b/src/domain/native/objcSwiftMetadata.test.ts index 57223f4a4e..e2fa3b765b 100644 --- a/src/domain/native/objcSwiftMetadata.test.ts +++ b/src/domain/native/objcSwiftMetadata.test.ts @@ -1,150 +1,8 @@ -import { fc } from "@fast-check/vitest"; import { describe, expect, it } from "vitest"; -import { - countMethodsByType, - isDbSaveComplete, - isGetterSelector, - isSetterSelector, - inspectNativeDispatchMetadata, - propertyNameFromSelector, - swiftDeclsByKind, - type ObjcMethod, - type SwiftDecl, - type DbSaveResult, -} from "./objcSwiftMetadata.js"; +import { inspectNativeDispatchMetadata } from "./objcSwiftMetadata.js"; describe("ObjC/Swift metadata", () => { - it("identifies getter selectors", () => { - expect(isGetterSelector("name")).toBe(true); - expect(isGetterSelector("setName:")).toBe(false); - expect(isGetterSelector("set")).toBe(true); - }); - - it("identifies setter selectors", () => { - expect(isSetterSelector("setName:")).toBe(true); - expect(isSetterSelector("name")).toBe(false); - }); - - it("extracts property name from selector", () => { - expect(propertyNameFromSelector("name")).toBe("name"); - expect(propertyNameFromSelector("setName:")).toBe("name"); - expect(propertyNameFromSelector("set")).toBe("set"); - }); - - it("counts methods by type", () => { - const methods: ObjcMethod[] = [ - { - selector: "init", - method_type: "instance", - address: 0x1000, - is_required: false, - is_optional: false, - }, - { - selector: "alloc", - method_type: "class", - address: 0x2000, - is_required: false, - is_optional: false, - }, - ]; - const counts = countMethodsByType(methods); - expect(counts.instance).toBe(1); - expect(counts.class).toBe(1); - expect(counts.ivar_getter).toBe(0); - expect(counts.ivar_setter).toBe(0); - }); - - it("filters Swift declarations by kind", () => { - const decls: SwiftDecl[] = [ - { - kind: "class", - name: "Foo", - module: null, - access_level: "public", - super_class: null, - protocols: [], - is_final: false, - is_required: false, - is_convenience_init: false, - is_override: false, - }, - ]; - const classes = swiftDeclsByKind(decls, "class"); - expect(classes).toHaveLength(1); - expect(classes[0]!.name).toBe("Foo"); - }); - - it("checks database save completeness", () => { - const result: DbSaveResult = { - operation: "save", - succeeded: true, - preserved_names: 10, - preserved_comments: 5, - preserved_bookmarks: 3, - database_path: "/tmp/db.hop", - error: null, - }; - expect(isDbSaveComplete(result, 10, 5, 3)).toBe(true); - expect(isDbSaveComplete(result, 11, 5, 3)).toBe(false); - }); - - it("is monotone in every expected count and total at the observed count", () => { - fc.assert( - fc.property( - fc.record({ - operation: fc.constantFrom( - "save", - "readback", - "open_database", - "close_database", - "export", - "import", - ), - succeeded: fc.boolean(), - preserved_names: fc.nat({ max: 50 }), - preserved_comments: fc.nat({ max: 50 }), - preserved_bookmarks: fc.nat({ max: 50 }), - database_path: fc.option(fc.string(), { nil: null }), - error: fc.option(fc.string(), { nil: null }), - }), - fc.nat({ max: 50 }), - fc.nat({ max: 50 }), - fc.nat({ max: 50 }), - (result, names, comments, bookmarks) => { - // Completeness is downward closed: if demanding more was met, - // demanding less must also be met. - if (isDbSaveComplete(result, names + 1, comments, bookmarks)) { - expect(isDbSaveComplete(result, names, comments, bookmarks)).toBe( - true, - ); - } - if (isDbSaveComplete(result, names, comments + 1, bookmarks)) { - expect(isDbSaveComplete(result, names, comments, bookmarks)).toBe( - true, - ); - } - if (isDbSaveComplete(result, names, comments, bookmarks + 1)) { - expect(isDbSaveComplete(result, names, comments, bookmarks)).toBe( - true, - ); - } - // Expecting exactly what was preserved is complete iff the save worked. - expect( - isDbSaveComplete( - result, - result.preserved_names, - result.preserved_comments, - result.preserved_bookmarks, - ), - ).toBe(result.succeeded); - }, - ), - { numRuns: 200 }, - ); - }); - it("bounds symbol projections and labels symbol-only metadata as partial", () => { const result = inspectNativeDispatchMetadata( [ diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index 17079d9fd9..5a0f0e0528 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -7,7 +7,6 @@ export const objcMethodTypeSchema = z.enum([ "ivar_getter", "ivar_setter", ]); -export type ObjcMethodType = z.infer; /** Objective-C property attribute. */ export const objcPropertyAttributeSchema = z.strictObject({ @@ -27,7 +26,7 @@ export const objcMethodSchema = z.strictObject({ is_required: z.boolean().default(false), is_optional: z.boolean().default(false), }); -export type ObjcMethod = z.infer; +type ObjcMethod = z.infer; /** Objective-C property metadata. */ export const objcPropertySchema = z.strictObject({ @@ -87,7 +86,7 @@ export const objcClassSchema = z.strictObject({ ) .optional(), }); -export type ObjcClass = z.infer; +type ObjcClass = z.infer; /** Exact location of a decoded native metadata value when the provider knows it. */ export const nativeMetadataLocationSchema = z.strictObject({ @@ -162,7 +161,7 @@ export const objcDispatchImplementationSchema = z.strictObject({ decode: nativeMetadataDecodeSchema, evidence: z.array(nativeMetadataEvidenceSchema).min(1), }); -export type ObjcDispatchImplementation = z.infer< +type ObjcDispatchImplementation = z.infer< typeof objcDispatchImplementationSchema >; @@ -224,7 +223,7 @@ export const swiftSymbolSchema = z.strictObject({ decode: nativeMetadataDecodeSchema, evidence: z.array(nativeMetadataEvidenceSchema).min(1), }); -export type SwiftSymbol = z.infer; +type SwiftSymbol = z.infer; /** Per-facet coverage; empty arrays are meaningful only with this record. */ export const nativeMetadataCoverageSchema = z.strictObject({ @@ -246,7 +245,6 @@ export const swiftDeclKindSchema = z.enum([ "global_var", "global_func", ]); -export type SwiftDeclKind = z.infer; /** Swift access level. */ export const swiftAccessLevelSchema = z.enum([ @@ -269,7 +267,6 @@ export const swiftDeclSchema = z.strictObject({ is_convenience_init: z.boolean().default(false), is_override: z.boolean().default(false), }); -export type SwiftDecl = z.infer; /** Database save/read operation type. */ export const dbOperationSchema = z.enum([ @@ -290,7 +287,6 @@ export const dbSaveResultSchema = z.strictObject({ database_path: z.string().nullable(), error: z.string().nullable(), }); -export type DbSaveResult = z.infer; /** Result of deeper ObjC/Swift metadata extraction. */ export const objcSwiftMetadataSchema = z.strictObject({ @@ -479,60 +475,3 @@ const parseMetadataAddress = (address: string | null): number | null => { const value = Number.parseInt(match[1], 16); return Number.isSafeInteger(value) ? value : null; }; - -/** Check if a selector name looks like a getter. */ -export function isGetterSelector(selector: string): boolean { - return /^[a-z][a-zA-Z0-9_]*$/u.test(selector) && !selector.includes(":"); -} - -/** Check if a selector name looks like a setter. */ -export function isSetterSelector(selector: string): boolean { - return selector.startsWith("set") && selector.endsWith(":"); -} - -/** Extract property name from a getter/setter selector. */ -export function propertyNameFromSelector(selector: string): string | null { - if (isSetterSelector(selector)) { - const inner = selector.slice(3, -1); - return inner.charAt(0).toLowerCase() + inner.slice(1); - } - if (isGetterSelector(selector)) { - return selector; - } - return null; -} - -/** Count ObjC methods by type. */ -export function countMethodsByType( - methods: readonly ObjcMethod[], -): Record { - return { - instance: methods.filter((m) => m.method_type === "instance").length, - class: methods.filter((m) => m.method_type === "class").length, - ivar_getter: methods.filter((m) => m.method_type === "ivar_getter").length, - ivar_setter: methods.filter((m) => m.method_type === "ivar_setter").length, - }; -} - -/** Get all Swift declarations of a specific kind. */ -export function swiftDeclsByKind( - decls: readonly SwiftDecl[], - kind: SwiftDeclKind, -): SwiftDecl[] { - return decls.filter((d) => d.kind === kind); -} - -/** Check if a database save preserved all expected items. */ -export function isDbSaveComplete( - result: DbSaveResult, - expectedNames: number, - expectedComments: number, - expectedBookmarks: number, -): boolean { - return ( - result.succeeded && - result.preserved_names >= expectedNames && - result.preserved_comments >= expectedComments && - result.preserved_bookmarks >= expectedBookmarks - ); -} diff --git a/src/domain/native/recordedCrash.test.ts b/src/domain/native/recordedCrash.test.ts new file mode 100644 index 0000000000..7a050d4170 --- /dev/null +++ b/src/domain/native/recordedCrash.test.ts @@ -0,0 +1,284 @@ +import { expect, it } from "vitest"; +import { + inspectRecordedCrashInputSchema, + recordedCrashSchema, +} from "./recordedCrash.js"; +import { + recordedCrashFixture, + recordedCrashSignalFixture, +} from "../../../tests/fixtures/binaryDiagnostics/recordedCrash.js"; + +it("preserves high uint64 registers and historical thread identity", () => { + const value = recordedCrashSchema.parse(recordedCrashFixture()); + expect( + value.threads[0]?.registers.find(({ name }) => name === "rdi")?.value, + ).toBe("0x1122334455667788"); + expect(value.live_process_identity).toBe("unknown"); + expect( + inspectRecordedCrashInputSchema.parse({ path: "/selected.core" }) + .include_debugger_context, + ).toBe(false); +}); + +it("preserves a short zero-filled note-segment tail", () => { + const value = recordedCrashSignalFixture(); + value.segments = value.segments.map((segment) => ({ + ...segment, + file_size: "0x19b", + })); + value.note_padding.push({ + segment_index: 0, + location: { offset: "0x218", bytes: "0x3" }, + bytes_base64: "AAAA", + }); + expect(recordedCrashSchema.safeParse(value).success).toBe(true); +}); + +it.each([ + "missing-thread", + "duplicate-thread", + "missing-signal", + "duplicate-signal", + "missing-register", + "unknown-register", + "duplicated-physical-thread", + "duplicated-physical-signal", + "omitted-physical-note", + "expanded-note-span", + "forged-padding", +])("rejects an incomplete or duplicate interpretation: %s", (scenario) => { + const value = recordedCrashSignalFixture(); + if ( + scenario === "duplicated-physical-thread" || + scenario === "duplicated-physical-signal" + ) { + const noteIndex = scenario === "duplicated-physical-thread" ? 0 : 1; + const note = value.notes[noteIndex]; + if (note === undefined) throw new Error("missing fixture note"); + value.notes.push({ ...note, index: 2 }); + if (noteIndex === 0) + value.threads.push( + ...value.threads.map((thread) => ({ ...thread, note_index: 2 })), + ); + else + value.signals.push( + ...value.signals.map((signal) => ({ ...signal, note_index: 2 })), + ); + } + if ( + ["omitted-physical-note", "expanded-note-span", "forged-padding"].includes( + scenario, + ) + ) { + value.notes = value.notes.filter((note) => note.index === 0); + value.signals = []; + } + if (scenario === "expanded-note-span") + value.notes = value.notes.map((note) => ({ + ...note, + location: { ...note.location, bytes: "0x198" }, + })); + if (scenario === "forged-padding") + value.note_padding.push({ + segment_index: 0, + location: { offset: "0x1e4", bytes: "0x34" }, + bytes_base64: Buffer.alloc(52).toString("base64"), + }); + if (scenario === "missing-thread") value.threads = []; + if (scenario === "duplicate-thread") + value.threads = [...value.threads, ...value.threads]; + if (scenario === "missing-signal") value.signals = []; + if (scenario === "duplicate-signal") + value.signals = [...value.signals, ...value.signals]; + if (scenario === "missing-register") + value.threads = value.threads.map((thread) => ({ + ...thread, + registers: thread.registers.slice(1), + })); + if (scenario === "unknown-register") + value.threads = value.threads.map((thread) => ({ + ...thread, + registers: thread.registers.map((register) => + register.name === "rdi" + ? { ...register, name: "unrecognized" } + : register, + ), + })); + expect(recordedCrashSchema.safeParse(value).success).toBe(false); +}); + +it.each([ + [ + "unsafe register number", + (v: ReturnType) => ({ + ...v, + threads: [ + { + ...v.threads[0], + registers: [ + { + name: "rdi", + value: 9007199254740992, + location: { offset: "0x104", bytes: "0x8" }, + }, + ], + }, + ], + }), + ], + [ + "source outside artifact", + (v: ReturnType) => ({ + ...v, + artifact: { ...v.artifact, bytes: 120 }, + }), + ], + [ + "invalid note reference", + (v: ReturnType) => ({ + ...v, + threads: [{ ...v.threads[0], note_index: 1 }], + }), + ], + [ + "register outside descriptor", + (v: ReturnType) => ({ + ...v, + threads: [ + { + ...v.threads[0], + registers: [ + { + name: "rdi", + value: "0x0", + location: { offset: "0x1e4", bytes: "0x8" }, + }, + ], + }, + ], + }), + ], + [ + "raw descriptor length", + (v: ReturnType) => ({ + ...v, + notes: v.notes.map((n) => ({ ...n, descriptor_bytes_base64: "AA==" })), + }), + ], + [ + "different owner ABI", + (v: ReturnType) => ({ + ...v, + notes: v.notes.map((n) => ({ ...n, owner_display: "OTHER" })), + }), + ], + [ + "raw owner ABI hidden by a CORE display label", + (v: ReturnType) => ({ + ...v, + notes: v.notes.map((n) => ({ + ...n, + owner_bytes_base64: Buffer.from("EVIL\0").toString("base64"), + })), + }), + ], + [ + "duplicate register", + (v: ReturnType) => ({ + ...v, + threads: v.threads.map((t) => ({ + ...t, + registers: [...t.registers, ...t.registers], + })), + }), + ], +] as const)("rejects %s before Evidence serialization", (_name, change) => + expect( + recordedCrashSchema.safeParse(change(recordedCrashFixture())).success, + ).toBe(false), +); + +it("does not interpret a signal through a mislabeled raw owner ABI", () => { + const value = recordedCrashFixture(); + expect( + recordedCrashSchema.safeParse({ + ...value, + threads: [], + notes: value.notes.map((note) => ({ + ...note, + type: "NT_SIGINFO", + owner_bytes_base64: Buffer.from("EVIL\0").toString("base64"), + })), + signals: [ + { + note_index: 0, + number: 11, + code: 1, + errno: 0, + fault_address: "0x123", + fault_address_meaning: "recorded-sigsegv-address", + thread_association: "unknown", + }, + ], + }).success, + ).toBe(false); +}); + +it("keeps absent map flags unknown and validates reported permission bits", () => { + const value = recordedCrashFixture(); + const context = { + status: "available", + gdb_version: "fixture", + pwndbg_version: "fixture", + connection: "core", + executable: null, + historical_pid: 123, + reported_limits: { + address_space_bytes: 3221225472, + cpu_seconds: 30, + file_size_bytes: 67108864, + }, + maps: [ + { + start: "0x1000", + end: "0x2000", + offset: "0x0", + reported_flags: 0, + permissions: null, + pathname_display: "recorded pathname", + current_file_identity: "unknown", + }, + ], + confidence: "derived", + completeness: "unknown", + diagnostics: value.diagnostics, + } as const; + expect( + recordedCrashSchema.safeParse({ ...value, debugger: context }).success, + ).toBe(true); + expect( + recordedCrashSchema.safeParse({ + ...value, + debugger: { + ...context, + maps: context.maps.map((m) => ({ + ...m, + permissions: { read: false, write: false, execute: false }, + })), + }, + }).success, + ).toBe(false); + expect( + recordedCrashSchema.safeParse({ + ...value, + debugger: { + ...context, + maps: context.maps.map((m) => ({ + ...m, + reported_flags: 4, + permissions: { read: false, write: false, execute: false }, + })), + }, + }).success, + ).toBe(false); +}); diff --git a/src/domain/native/recordedCrash.ts b/src/domain/native/recordedCrash.ts new file mode 100644 index 0000000000..7668e63ce2 --- /dev/null +++ b/src/domain/native/recordedCrash.ts @@ -0,0 +1,362 @@ +import { z } from "zod"; +import { recordedNoteCoverageIssues } from "./recordedCrashNoteCoverage.js"; + +const hex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/); +const index = z.number().int().nonnegative(); +const signed32 = z.number().int().min(-2147483648).max(2147483647); +const base64 = z + .string() + .regex( + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$/, + ); +const range = z.strictObject({ offset: hex, bytes: hex }); +const diagnostics = z.strictObject({ + stdout: z.string(), + stderr: z.string(), + truncated: z.boolean(), +}); +// Exact b"CORE\0"; a lossy display label cannot establish the Linux note ABI. +const LINUX_CORE_OWNER_BASE64 = "Q09SRQA="; + +/** Complete register identities in the supported Linux amd64 PRSTATUS ABI order. */ +export const RECORDED_LINUX_AMD64_REGISTERS: readonly string[] = [ + "r15", + "r14", + "r13", + "r12", + "rbp", + "rbx", + "r11", + "r10", + "r9", + "r8", + "rax", + "rcx", + "rdx", + "rsi", + "rdi", + "orig_rax", + "rip", + "cs", + "eflags", + "rsp", + "ss", + "fs_base", + "gs_base", + "ds", + "es", + "fs", + "gs", +]; +const supportedRegisters = new Set(RECORDED_LINUX_AMD64_REGISTERS); + +/** Select an explicit recording and optionally request debugger-derived mapping context. */ +export const inspectRecordedCrashInputSchema = z.strictObject({ + path: z + .string() + .min(1) + .describe("Absolute filesystem path to a supplied Linux ELF core"), + include_debugger_context: z + .boolean() + .default(false) + .describe( + "Add core-only mapping candidates through caller-supplied GDB/pwndbg", + ), +}); + +/** Debugger map candidates preserve reported flags; zero does not establish absent permissions. */ +export const recordedCrashDebuggerSchema = z.discriminatedUnion("status", [ + z.strictObject({ status: z.literal("not-requested") }), + z.strictObject({ + status: z.literal("available"), + gdb_version: z.string().min(1), + pwndbg_version: z.string().min(1), + connection: z.literal("core"), + executable: z.null(), + historical_pid: signed32, + reported_limits: z.strictObject({ + address_space_bytes: index, + cpu_seconds: index, + file_size_bytes: index, + }), + maps: z.array( + z.strictObject({ + start: hex, + end: hex, + offset: hex, + reported_flags: index, + permissions: z + .strictObject({ + read: z.boolean(), + write: z.boolean(), + execute: z.boolean(), + }) + .nullable(), + pathname_display: z.string(), + current_file_identity: z.literal("unknown"), + }), + ), + confidence: z.literal("derived"), + completeness: z.literal("unknown"), + diagnostics, + }), +]); + +/** Portable recorded observations; registers and source coordinates remain lossless. */ +const recordedCrashObjectSchema = z.strictObject({ + artifact: z.strictObject({ + path: z.string().min(1), + sha256: z.string().regex(/^[a-f0-9]{64}$/), + bytes: index, + }), + format: z.literal("elf-core"), + architecture: z.literal("x86_64-little-endian"), + target_execution: z.literal("not-performed"), + live_process_identity: z.literal("unknown"), + segments: z.array( + z.strictObject({ + index, + type: z.union([z.string(), index]), + header_location: range, + offset: hex, + file_size: hex, + memory_size: hex, + virtual_address: hex, + physical_address: hex, + alignment: hex, + flags: hex, + file_backing: z.enum(["file", "none"]), + }), + ), + notes: z.array( + z.strictObject({ + index, + segment_index: index, + type: z.union([z.string(), index]), + location: range, + owner_location: range, + owner_bytes_base64: base64, + owner_display: z.string().nullable(), + descriptor_location: range, + descriptor_bytes_base64: base64, + }), + ), + note_padding: z.array( + z.strictObject({ + segment_index: index, + location: range, + bytes_base64: base64, + }), + ), + threads: z.array( + z.strictObject({ + note_index: index, + historical_pid: signed32, + recorded_current_signal: z.number().int().min(-32768).max(32767), + registers: z.array( + z.strictObject({ + name: z.string().min(1), + value: hex, + location: range, + }), + ), + }), + ), + signals: z.array( + z.strictObject({ + note_index: index, + number: signed32, + code: signed32, + errno: signed32, + fault_address: hex.nullable(), + fault_address_meaning: z.enum(["recorded-sigsegv-address", "unknown"]), + thread_association: z.literal("unknown"), + }), + ), + debugger: recordedCrashDebuggerSchema, + note_interpretation_completeness: z.literal("unknown"), + decoder_diagnostics: diagnostics, + diagnostics, + limitations: z.array(z.string()), +}); + +/** Provider payload excludes identity and diagnostics bound by the owned snapshot boundary. */ +export const recordedCrashPayloadSchema = recordedCrashObjectSchema.omit({ + artifact: true, + diagnostics: true, + debugger: true, + decoder_diagnostics: true, +}); + +/** Validate the next consumer's file ranges, note links and register representations. */ +export const recordedCrashSchema = recordedCrashObjectSchema.superRefine( + (value, context) => { + const fail = (message: string) => + context.addIssue({ code: "custom", message }); + const within = ( + item: z.output, + start = 0n, + end = BigInt(value.artifact.bytes), + ) => { + const offset = BigInt(item.offset); + const bytes = BigInt(item.bytes); + return offset >= start && offset <= end && bytes <= end - offset; + }; + for (const issue of recordedNoteCoverageIssues( + value.segments, + value.notes, + value.note_padding, + )) + fail(issue); + for (const [position, segment] of value.segments.entries()) { + if (position !== segment.index || !within(segment.header_location)) + fail("Invalid original segment identity or header range."); + if ( + segment.file_backing === "file" && + !within({ offset: segment.offset, bytes: segment.file_size }) + ) + fail("Recorded segment exceeds artifact bytes."); + if ( + segment.type === "PT_LOAD" && + BigInt(segment.file_size) > BigInt(segment.memory_size) + ) + fail("Recorded PT_LOAD file size exceeds memory size."); + } + for (const [position, note] of value.notes.entries()) { + const segment = value.segments[note.segment_index]; + if (position !== note.index || segment?.type !== "PT_NOTE") { + fail("Invalid original note identity or segment reference."); + continue; + } + const decodedLength = (raw: string) => + BigInt( + (raw.length / 4) * 3 - + (raw.endsWith("==") ? 2 : raw.endsWith("=") ? 1 : 0), + ); + if ( + decodedLength(note.owner_bytes_base64) !== + BigInt(note.owner_location.bytes) || + decodedLength(note.descriptor_bytes_base64) !== + BigInt(note.descriptor_location.bytes) + ) + fail("Raw note lengths differ from their source ranges."); + const start = BigInt(segment.offset); + const end = start + BigInt(segment.file_size); + if ( + !within(note.location, start, end) || + !within( + note.owner_location, + BigInt(note.location.offset), + BigInt(note.location.offset) + BigInt(note.location.bytes), + ) || + !within( + note.descriptor_location, + BigInt(note.location.offset), + BigInt(note.location.offset) + BigInt(note.location.bytes), + ) + ) + fail("Note source ranges exceed their recorded segment."); + } + for (const padding of value.note_padding) { + const segment = value.segments[padding.segment_index]; + if ( + segment?.type !== "PT_NOTE" || + !within( + padding.location, + BigInt(segment.offset), + BigInt(segment.offset) + BigInt(segment.file_size), + ) + ) + fail("Invalid recorded note padding range."); + } + for (const [kind, rows] of [ + ["NT_PRSTATUS", value.threads], + ["NT_SIGINFO", value.signals], + ] as const) { + const matchingNotes = value.notes.filter( + (note) => + note.type === kind && + note.owner_bytes_base64 === LINUX_CORE_OWNER_BASE64, + ); + if ( + rows.length !== matchingNotes.length || + new Set(rows.map(({ note_index }) => note_index)).size !== rows.length + ) + fail( + "Each supported raw note requires exactly one corresponding interpretation.", + ); + } + for (const thread of value.threads) { + const note = value.notes[thread.note_index]; + if ( + note?.type !== "NT_PRSTATUS" || + note.owner_display !== "CORE" || + note.owner_bytes_base64 !== LINUX_CORE_OWNER_BASE64 + ) { + fail("Thread references a different note kind."); + continue; + } + if ( + thread.registers.length !== RECORDED_LINUX_AMD64_REGISTERS.length || + !thread.registers.every(({ name }) => supportedRegisters.has(name)) || + new Set(thread.registers.map(({ name }) => name)).size !== + thread.registers.length + ) + fail( + "Each recorded thread requires every supported register exactly once.", + ); + for (const register of thread.registers) + if ( + BigInt(register.location.bytes) !== 8n || + !within( + register.location, + BigInt(note.descriptor_location.offset), + BigInt(note.descriptor_location.offset) + + BigInt(note.descriptor_location.bytes), + ) + ) + fail("Register source exceeds its descriptor."); + } + for (const signal of value.signals) { + if ( + value.notes[signal.note_index]?.type !== "NT_SIGINFO" || + value.notes[signal.note_index]?.owner_display !== "CORE" || + value.notes[signal.note_index]?.owner_bytes_base64 !== + LINUX_CORE_OWNER_BASE64 + ) + fail("Signal references a different note kind."); + const known = + signal.number === 11 && (signal.code === 1 || signal.code === 2); + if ( + known !== (signal.fault_address !== null) || + (signal.fault_address !== null) !== + (signal.fault_address_meaning === "recorded-sigsegv-address") + ) + fail("Signal union meaning does not match recorded SIGSEGV code."); + } + if (value.debugger.status === "available") + for (const map of value.debugger.maps) { + if (BigInt(map.end) < BigInt(map.start)) + fail("Debugger mapping end precedes its start."); + if ( + map.permissions !== null && + (map.permissions.read !== Boolean(map.reported_flags & 4) || + map.permissions.write !== Boolean(map.reported_flags & 2) || + map.permissions.execute !== Boolean(map.reported_flags & 1)) + ) + fail("Mapping permissions differ from reported flags."); + if ( + (map.reported_flags === 0 || map.reported_flags > 7) !== + (map.permissions === null) + ) + fail( + "Unreported or unfamiliar map flags require unknown permissions.", + ); + } + }, +); + +export type InspectRecordedCrashInput = z.output< + typeof inspectRecordedCrashInputSchema +>; +export type RecordedCrash = z.output; diff --git a/src/domain/native/recordedCrashNoteCoverage.ts b/src/domain/native/recordedCrashNoteCoverage.ts new file mode 100644 index 0000000000..dbe7e51cd4 --- /dev/null +++ b/src/domain/native/recordedCrashNoteCoverage.ts @@ -0,0 +1,111 @@ +type Range = { readonly offset: string; readonly bytes: string }; +type Segment = { + readonly index: number; + readonly type: string | number; + readonly offset: string; + readonly file_size: string; +}; +type Note = { + readonly segment_index: number; + readonly location: Range; + readonly owner_location: Range; + readonly descriptor_location: Range; +}; +type Padding = { + readonly segment_index: number; + readonly location: Range; + readonly bytes_base64: string; +}; +const align = (size: bigint) => ((size + 3n) / 4n) * 4n; +const layoutMatches = (note: Note): boolean => { + const start = BigInt(note.location.offset); + const owner = BigInt(note.owner_location.bytes); + const descriptor = BigInt(note.descriptor_location.bytes); + return ( + BigInt(note.owner_location.offset) === start + 12n && + BigInt(note.descriptor_location.offset) === start + 12n + align(owner) && + BigInt(note.location.bytes) === 12n + align(owner) + align(descriptor) + ); +}; +const paddingMatches = ( + item: Padding, + segment: Segment | undefined, +): boolean => { + const bytes = BigInt(item.location.bytes); + const decodedLength = + (item.bytes_base64.length / 4) * 3 - + (item.bytes_base64.endsWith("==") + ? 2 + : item.bytes_base64.endsWith("=") + ? 1 + : 0); + return ( + bytes > 0n && + bytes < 12n && + BigInt(decodedLength) === bytes && + /^A*={0,2}$/.test(item.bytes_base64) && + segment !== undefined && + BigInt(item.location.offset) + bytes === + BigInt(segment.offset) + BigInt(segment.file_size) + ); +}; + +/** Require exact aligned note layouts and one complete, nonoverlapping physical coverage. */ +export function recordedNoteCoverageIssues( + segments: readonly Segment[], + notes: readonly Note[], + padding: readonly Padding[], +): readonly string[] { + const issues: string[] = []; + const bySegment = new Map(); + const all: Range[] = []; + const add = (segment: number, range: Range) => { + const ranges = bySegment.get(segment) ?? []; + ranges.push(range); + bySegment.set(segment, ranges); + all.push(range); + }; + for (const note of notes) { + if (!layoutMatches(note)) + issues.push( + "Recorded note layout differs from its aligned header/owner/descriptor ranges.", + ); + add(note.segment_index, note.location); + } + for (const item of padding) { + if (!paddingMatches(item, segments[item.segment_index])) + issues.push( + "Recorded note padding must be a short zero-filled segment tail.", + ); + add(item.segment_index, item.location); + } + const compare = (left: Range, right: Range) => + BigInt(left.offset) < BigInt(right.offset) + ? -1 + : BigInt(left.offset) > BigInt(right.offset) + ? 1 + : 0; + let end = 0n; + for (const range of all.sort(compare)) { + const start = BigInt(range.offset); + if (start < end) + issues.push("Physical recorded note/padding ranges overlap or repeat."); + if (start + BigInt(range.bytes) > end) end = start + BigInt(range.bytes); + } + for (const segment of segments) { + if (segment.type !== "PT_NOTE") continue; + let cursor = BigInt(segment.offset); + for (const range of (bySegment.get(segment.index) ?? []).sort(compare)) { + if (BigInt(range.offset) !== cursor) + issues.push( + "Recorded note/padding ranges leave a gap or overlap in their segment.", + ); + cursor = BigInt(range.offset) + BigInt(range.bytes); + } + if (cursor !== BigInt(segment.offset) + BigInt(segment.file_size)) + issues.push( + "Recorded note/padding ranges do not completely cover their segment.", + ); + } + return issues; +} diff --git a/src/domain/packageAnalysis.test.ts b/src/domain/packageAnalysis.test.ts deleted file mode 100644 index b182feb535..0000000000 --- a/src/domain/packageAnalysis.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - classifyResource, - detectPackageFormat, - hasValidSignature, - isConfigurationResource, - isManifestResource, - resourcesByType, - totalResourceSize, - type PackageManifest, -} from "./packageAnalysis.js"; - -describe("package analysis", () => { - it("detects package format from extension", () => { - expect(detectPackageFormat("app.msix")).toBe("msix"); - expect(detectPackageFormat("installer.msi")).toBe("msi"); - expect(detectPackageFormat("app.appx")).toBe("appx"); - expect(detectPackageFormat("app.zip")).toBeNull(); - }); - - it("identifies manifest resources", () => { - expect(isManifestResource("AppXManifest.xml")).toBe(true); - expect(isManifestResource("resources.pri")).toBe(false); - }); - - it("identifies configuration resources", () => { - expect(isConfigurationResource("settings.xml")).toBe(true); - expect(isConfigurationResource("config.json")).toBe(true); - expect(isConfigurationResource("app.exe")).toBe(false); - }); - - it("classifies resources by path", () => { - expect(classifyResource("AppXManifest.xml")).toBe("manifest"); - expect(classifyResource("icon.png")).toBe("icon"); - expect(classifyResource("settings.xml")).toBe("configuration"); - expect(classifyResource("cert.cer")).toBe("certificate"); - expect(classifyResource("app.exe")).toBe("executable"); - expect(classifyResource("lib.dll")).toBe("library"); - expect(classifyResource("data.bin")).toBe("other"); - }); - - const validManifest: PackageManifest = { - format: "msix", - name: "TestApp", - publisher: "CN=TestPublisher", - version: "1.0.0.0", - architecture: "x64", - resources: [ - { - path: "AppXManifest.xml", - type: "manifest", - size: 1000, - digest: "a".repeat(64), - }, - { path: "icon.png", type: "icon", size: 5000, digest: "b".repeat(64) }, - { - path: "app.exe", - type: "executable", - size: 50000, - digest: "c".repeat(64), - }, - ], - signature: { - is_signed: true, - signer_subject: "CN=TestPublisher", - certificate_thumbprint: "abc123", - is_valid: true, - is_timestamped: true, - timestamp_signer: "CN=Timestamp", - }, - is_framework: false, - dependencies: ["Microsoft.VCLibs.14.00"], - capabilities: ["internetClient", "localNetwork"], - }; - - it("filters resources by type", () => { - const icons = resourcesByType(validManifest, "icon"); - expect(icons).toHaveLength(1); - expect(icons[0]!.path).toBe("icon.png"); - }); - - it("computes total resource size", () => { - expect(totalResourceSize(validManifest)).toBe(56000); - }); - - it("checks valid signature", () => { - expect(hasValidSignature(validManifest)).toBe(true); - const unsigned: PackageManifest = { - ...validManifest, - signature: { ...validManifest.signature, is_valid: false }, - }; - expect(hasValidSignature(unsigned)).toBe(false); - }); -}); diff --git a/src/domain/packageAnalysis.ts b/src/domain/packageAnalysis.ts deleted file mode 100644 index 4eed306521..0000000000 --- a/src/domain/packageAnalysis.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { z } from "zod"; - -/** Supported Windows package formats. */ -export const packageFormatSchema = z.enum(["msix", "msi", "appx"]); -export type PackageFormat = z.infer; - -/** A resource entry within a package. */ -export const packageResourceSchema = z.strictObject({ - /** Resource path within the package. */ - path: z.string().min(1), - /** Resource type. */ - type: z.enum([ - "manifest", - "icon", - "configuration", - "certificate", - "executable", - "library", - "data", - "other", - ]), - /** Size in bytes. */ - size: z.number().int().nonnegative(), - /** SHA-256 digest. */ - digest: z - .string() - .regex(/^[a-f0-9]{64}$/u) - .nullable(), -}); -export type PackageResource = z.infer; - -/** Digital signature information. */ -export const digitalSignatureSchema = z.strictObject({ - /** Whether the package is signed. */ - is_signed: z.boolean(), - /** Signer subject name. */ - signer_subject: z.string().nullable(), - /** Certificate thumbprint. */ - certificate_thumbprint: z.string().nullable(), - /** Whether the signature is valid. */ - is_valid: z.boolean().default(false), - /** Whether the signature is timestamped. */ - is_timestamped: z.boolean().default(false), - /** Timestamp signer if present. */ - timestamp_signer: z.string().nullable(), -}); - -/** A Windows package manifest for static analysis. */ -export const packageManifestSchema = z.strictObject({ - /** Package format. */ - format: packageFormatSchema, - /** Package identity name. */ - name: z.string().min(1), - /** Package publisher. */ - publisher: z.string().nullable(), - /** Package version. */ - version: z.string().nullable(), - /** Architecture. */ - architecture: z.enum(["x86", "x64", "arm", "arm64", "neutral", "unknown"]), - /** Package resources. */ - resources: z.array(packageResourceSchema).default([]), - /** Digital signature. */ - signature: digitalSignatureSchema, - /** Whether the package is a framework. */ - is_framework: z.boolean().default(false), - /** Package dependencies. */ - dependencies: z.array(z.string()).default([]), - /** Capabilities declared in the manifest. */ - capabilities: z.array(z.string()).default([]), -}); -export type PackageManifest = z.infer; - -/** Detect package format from file extension. */ -export function detectPackageFormat(path: string): PackageFormat | null { - const lower = path.toLowerCase(); - if (lower.endsWith(".msix")) return "msix"; - if (lower.endsWith(".msi")) return "msi"; - if (lower.endsWith(".appx")) return "appx"; - return null; -} - -/** Check if a resource path looks like a manifest. */ -export function isManifestResource(path: string): boolean { - const lower = path.toLowerCase(); - return ( - lower.includes("appxmanifest.xml") || - lower.includes("appxmanifest") || - lower.includes("manifest.xml") || - lower.endsWith("_manifest.xml") - ); -} - -/** Check if a resource path looks like a configuration file. */ -export function isConfigurationResource(path: string): boolean { - const lower = path.toLowerCase(); - return ( - lower.endsWith(".xml") || - lower.endsWith(".json") || - lower.endsWith(".ini") || - lower.endsWith(".config") - ); -} - -/** Classify a resource by its path. */ -export function classifyResource(path: string): PackageResource["type"] { - if (isManifestResource(path)) return "manifest"; - const lower = path.toLowerCase(); - if ( - lower.endsWith(".ico") || - lower.endsWith(".png") || - lower.endsWith(".jpg") || - lower.endsWith(".jpeg") - ) - return "icon"; - if (isConfigurationResource(path)) return "configuration"; - if ( - lower.endsWith(".cer") || - lower.endsWith(".pfx") || - lower.endsWith(".p12") - ) - return "certificate"; - if (lower.endsWith(".exe") || lower.endsWith(".dll")) - return lower.endsWith(".exe") ? "executable" : "library"; - return "other"; -} - -/** Get all resources of a specific type. */ -export function resourcesByType( - manifest: PackageManifest, - type: PackageResource["type"], -): PackageResource[] { - return manifest.resources.filter((r) => r.type === type); -} - -/** Get the total size of all resources. */ -export function totalResourceSize(manifest: PackageManifest): number { - return manifest.resources.reduce((sum, r) => sum + r.size, 0); -} - -/** Check if a package has a valid signature. */ -export function hasValidSignature(manifest: PackageManifest): boolean { - return manifest.signature.is_signed && manifest.signature.is_valid; -} diff --git a/src/domain/peInspection.test.ts b/src/domain/peInspection.test.ts deleted file mode 100644 index c051414878..0000000000 --- a/src/domain/peInspection.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - countExecutableSections, - getExportedFunctions, - getImportedDlls, - hasPdbInfo, - parseDebugType, - parseImageCharacteristics, - parseMachineType, - parseSectionCharacteristics, - peManifestSchema, - type PeManifest, -} from "./peInspection.js"; - -const validManifest: PeManifest = { - dos_magic: 0x5a4d, - pe_signature: 0x00004550, - machine: "x64", - number_of_sections: 3, - timestamp: 0x12345678, - is_dll: false, - is_executable: true, - is_console: true, - entry_point: 0x1000, - image_base: 0x400000, - section_alignment: 0x1000, - subsystem_version: "10.0", - sections: [ - { - name: ".text", - virtual_size: 0x1000, - virtual_address: 0x1000, - raw_size: 0x1000, - raw_offset: 0x400, - characteristics: { - has_code: true, - has_initialized_data: false, - has_uninitialized_data: false, - readable: true, - writable: false, - executable: true, - }, - }, - { - name: ".data", - virtual_size: 0x500, - virtual_address: 0x2000, - raw_size: 0x500, - raw_offset: 0x1400, - characteristics: { - has_code: false, - has_initialized_data: true, - has_uninitialized_data: false, - readable: true, - writable: true, - executable: false, - }, - }, - { - name: ".rdata", - virtual_size: 0x200, - virtual_address: 0x3000, - raw_size: 0x200, - raw_offset: 0x1900, - characteristics: { - has_code: false, - has_initialized_data: true, - has_uninitialized_data: false, - readable: true, - writable: false, - executable: false, - }, - }, - ], - imports: [ - { - dll: "KERNEL32.DLL", - function_name: "CreateFileW", - ordinal: null, - is_delay_import: false, - }, - { - dll: "KERNEL32.DLL", - function_name: "WriteFile", - ordinal: null, - is_delay_import: false, - }, - { - dll: "USER32.DLL", - function_name: "MessageBoxW", - ordinal: 100, - is_delay_import: false, - }, - ], - exports: [ - { name: "DllMain", ordinal: 1, rva: 0x1000, forwarder: null }, - { name: "MyFunction", ordinal: 2, rva: 0x1050, forwarder: null }, - ], - resources: [ - { type: "RT_VERSION", name: "1", language: 0x0409, rva: 0x4000, size: 100 }, - ], - debug_entries: [ - { - type: "CODEVIEW", - rva: 0x5000, - size: 100, - pdb_guid: "AABBCCDD-EEFF-GGHH-IIJJ-KKLLMMNNOOPP", - pdb_age: 1, - pdb_path: "C:\\src\\app.pdb", - }, - ], - tls_callbacks: [{ callback_rva: 0x6000, index: 0 }], - relocations_count: 50, - image_characteristics: ["Executable", "Console"], -}; - -describe("PE/COFF static inspection", () => { - it("parses machine types", () => { - expect(parseMachineType(0x014c)).toBe("x86"); - expect(parseMachineType(0x8664)).toBe("x64"); - expect(parseMachineType(0x01c0)).toBe("arm"); - expect(parseMachineType(0xaa64)).toBe("arm64"); - expect(parseMachineType(0x0200)).toBe("ia64"); - expect(parseMachineType(0xffff)).toBe("unknown"); - }); - - it("parses section characteristics", () => { - const CODE = 0x00000020; - const READ = 0x40000000; - const EXECUTE = 0x20000000; - const result = parseSectionCharacteristics(CODE | READ | EXECUTE); - expect(result.has_code).toBe(true); - expect(result.readable).toBe(true); - expect(result.executable).toBe(true); - expect(result.writable).toBe(false); - }); - - it("parses image characteristics", () => { - const DLL = 0x2000; - const EXEC = 0x0002; - const flags = parseImageCharacteristics(DLL | EXEC); - expect(flags).toContain("DLL"); - expect(flags).toContain("Executable"); - }); - - it("parses debug types", () => { - expect(parseDebugType(2)).toBe("CODEVIEW"); - expect(parseDebugType(1)).toBe("COFF"); - expect(parseDebugType(99)).toBe("UNKNOWN"); - }); - - it("rejects a manifest whose section table is malformed", () => { - const malformed = { - ...validManifest, - sections: [{ ...validManifest.sections[0]!, name: 7 }], - }; - const result = peManifestSchema.safeParse(malformed); - expect(result.success).toBe(false); - }); - - it("accepts static manifests with more than 96 PE sections", () => { - const section = validManifest.sections[0]; - if (section === undefined) throw new Error("Expected PE section fixture"); - const sections = Array.from({ length: 97 }, (_, index) => ({ - ...section, - name: `section-${String(index)}`, - })); - - expect( - peManifestSchema.parse({ - ...validManifest, - number_of_sections: sections.length, - sections, - }).sections, - ).toHaveLength(97); - }); - - it("detects PDB info", () => { - expect(hasPdbInfo(validManifest)).toBe(true); - const withoutPdb: PeManifest = { - ...validManifest, - debug_entries: [ - { - type: "COFF", - rva: 0x5000, - size: 100, - pdb_guid: null, - pdb_age: null, - pdb_path: null, - }, - ], - }; - expect(hasPdbInfo(withoutPdb)).toBe(false); - }); - - it("gets imported DLLs", () => { - const dlls = getImportedDlls(validManifest); - expect(dlls).toHaveLength(2); - expect(dlls).toContain("KERNEL32.DLL"); - expect(dlls).toContain("USER32.DLL"); - }); - - it("gets exported functions", () => { - const exports = getExportedFunctions(validManifest); - expect(exports).toHaveLength(2); - expect(exports).toContain("DllMain"); - expect(exports).toContain("MyFunction"); - }); - - it("counts executable sections", () => { - expect(countExecutableSections(validManifest)).toBe(1); - }); -}); diff --git a/src/domain/peInspection.ts b/src/domain/peInspection.ts deleted file mode 100644 index 9e58dea0b5..0000000000 --- a/src/domain/peInspection.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { z } from "zod"; - -/** PE/COFF section characteristics flags. */ -export const sectionCharacteristicsSchema = z.object({ - /** Section contains executable code. */ - has_code: z.boolean().default(false), - /** Section contains initialized data. */ - has_initialized_data: z.boolean().default(false), - /** Section contains uninitialized data. */ - has_uninitialized_data: z.boolean().default(false), - /** Section can be read. */ - readable: z.boolean().default(false), - /** Section can be written. */ - writable: z.boolean().default(false), - /** Section can be executed. */ - executable: z.boolean().default(false), -}); - -/** PE/COFF section header. */ -export const peSectionSchema = z.strictObject({ - name: z.string().min(1), - virtual_size: z.number().int().nonnegative(), - virtual_address: z.number().int().nonnegative(), - raw_size: z.number().int().nonnegative(), - raw_offset: z.number().int().nonnegative(), - characteristics: sectionCharacteristicsSchema, -}); - -/** PE import entry. */ -export const peImportSchema = z.strictObject({ - dll: z.string().min(1), - function_name: z.string().min(1), - ordinal: z.number().int().nullable(), - is_delay_import: z.boolean().default(false), -}); - -/** PE export entry. */ -export const peExportSchema = z.strictObject({ - name: z.string().min(1), - ordinal: z.number().int().nonnegative(), - rva: z.number().int().nonnegative(), - forwarder: z.string().nullable(), -}); - -/** PE/COFF resource entry. */ -export const peResourceSchema = z.strictObject({ - type: z.string().min(1), - name: z.string().nullable(), - language: z.number().int().nullable(), - rva: z.number().int().nonnegative(), - size: z.number().int().nonnegative(), -}); - -/** PE debug directory entry. */ -export const peDebugEntrySchema = z.strictObject({ - type: z.string().min(1), - rva: z.number().int().nonnegative(), - size: z.number().int().nonnegative(), - /** PDB GUID if this is a CodeView entry. */ - pdb_guid: z.string().nullable(), - /** PDB age if this is a CodeView entry. */ - pdb_age: z.number().int().nullable(), - /** PDB path if this is a CodeView entry. */ - pdb_path: z.string().nullable(), -}); - -/** TLS callback entry. */ -export const peTlsCallbackSchema = z.strictObject({ - callback_rva: z.number().int().nonnegative(), - index: z.number().int().nonnegative(), -}); - -/** Architecture/machine type. */ -export const peMachineTypeSchema = z.enum([ - "x86", - "x64", - "arm", - "arm64", - "ia64", - "unknown", -]); -export type PeMachineType = z.infer; - -/** PE/COFF manifest for static inspection. */ -export const peManifestSchema = z.strictObject({ - /** DOS header magic. */ - dos_magic: z.literal(0x5a4d), - /** PE signature. */ - pe_signature: z.literal(0x00004550), - /** Machine type. */ - machine: peMachineTypeSchema, - /** Number of sections. */ - number_of_sections: z.number().int().positive(), - /** Timestamp from the COFF header. */ - timestamp: z.number().int().nullable(), - /** PE characteristics flags. */ - is_dll: z.boolean().default(false), - is_executable: z.boolean().default(false), - is_console: z.boolean().default(true), - /** Entry point RVA. */ - entry_point: z.number().int().nonnegative(), - /** Image base. */ - image_base: z.number().int().nonnegative(), - /** Section alignment. */ - section_alignment: z.number().int().positive(), - /** Subsystem version. */ - subsystem_version: z.string().nullable(), - /** Sections. */ - sections: z.array(peSectionSchema), - /** Imports. */ - imports: z.array(peImportSchema).default([]), - /** Exports. */ - exports: z.array(peExportSchema).default([]), - /** Resources. */ - resources: z.array(peResourceSchema).default([]), - /** Debug directory entries. */ - debug_entries: z.array(peDebugEntrySchema).default([]), - /** TLS callbacks. */ - tls_callbacks: z.array(peTlsCallbackSchema).default([]), - /** Relocations count (not all entries). */ - relocations_count: z.number().int().nonnegative().default(0), - /** Image file characteristics. */ - image_characteristics: z.array(z.string()).default([]), -}); -export type PeManifest = z.infer; - -/** Parse machine type from COFF machine field. */ -export function parseMachineType(machine: number): PeMachineType { - switch (machine) { - case 0x014c: - return "x86"; - case 0x8664: - return "x64"; - case 0x01c0: - case 0x01c2: - case 0x01c4: - return "arm"; - case 0xaa64: - return "arm64"; - case 0x0200: - return "ia64"; - default: - return "unknown"; - } -} - -/** Parse section characteristics flags. */ -export function parseSectionCharacteristics( - characteristics: number, -): z.infer { - const IMAGE_SCN_CNT_CODE = 0x00000020; - const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040; - const IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080; - const IMAGE_SCN_MEM_READ = 0x40000000; - const IMAGE_SCN_MEM_WRITE = 0x80000000; - const IMAGE_SCN_MEM_EXECUTE = 0x20000000; - - return { - has_code: (characteristics & IMAGE_SCN_CNT_CODE) !== 0, - has_initialized_data: - (characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) !== 0, - has_uninitialized_data: - (characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) !== 0, - readable: (characteristics & IMAGE_SCN_MEM_READ) !== 0, - writable: (characteristics & IMAGE_SCN_MEM_WRITE) !== 0, - executable: (characteristics & IMAGE_SCN_MEM_EXECUTE) !== 0, - }; -} - -/** Parse image characteristics flags. */ -export function parseImageCharacteristics(characteristics: number): string[] { - const IMAGE_DLL = 0x2000; - const IMAGE_EXECUTABLE = 0x0002; - const IMAGE_CONSOLE = 0x0003; - const flags: string[] = []; - if (characteristics & IMAGE_DLL) flags.push("DLL"); - if (characteristics & IMAGE_EXECUTABLE) flags.push("Executable"); - if ((characteristics & 0x0000000f) === IMAGE_CONSOLE) flags.push("Console"); - return flags; -} - -/** Parse a PE debug directory type. */ -export function parseDebugType(type: number): string { - switch (type) { - case 0: - return "UNKNOWN"; - case 1: - return "COFF"; - case 2: - return "CODEVIEW"; - case 3: - return "POGO"; - case 4: - return "MISC"; - default: - return "UNKNOWN"; - } -} - -/** Check if a PE manifest has PDB debug info. */ -export function hasPdbInfo(manifest: PeManifest): boolean { - return manifest.debug_entries.some( - (e) => e.pdb_guid !== null && e.pdb_path !== null, - ); -} - -/** Get all imported DLLs from a PE manifest. */ -export function getImportedDlls(manifest: PeManifest): string[] { - return [...new Set(manifest.imports.map((i) => i.dll))]; -} - -/** Get all exported function names from a PE manifest. */ -export function getExportedFunctions(manifest: PeManifest): string[] { - return manifest.exports.map((e) => e.name); -} - -/** Count executable sections. */ -export function countExecutableSections(manifest: PeManifest): number { - return manifest.sections.filter((s) => s.characteristics.executable).length; -} diff --git a/src/domain/process/processCapture.comparison.test.ts b/src/domain/process/processCapture.comparison.test.ts new file mode 100644 index 0000000000..93d7246270 --- /dev/null +++ b/src/domain/process/processCapture.comparison.test.ts @@ -0,0 +1,160 @@ +import { expect, it } from "vitest"; + +import { + compareUnverifiedProcessCaptures as compareProcessCaptures, + emptyProcessCapture as emptyCapture, +} from "./processCapture.fixture.js"; + +it("distinguishes added evidence from unknown observations", () => { + const capture = emptyCapture(); + const added = compareProcessCaptures(capture, { + ...capture, + frames: [{ sequence: 0, at_ms: 0, data: "new" }], + rendered_frames: [ + { + sequence: 0, + at_ms: 0, + columns: 3, + rows: 1, + cursor_x: 3, + cursor_y: 0, + active_buffer: "normal", + lines: ["new"], + serialized_state: "new", + }, + ], + }); + expect(added).toMatchObject({ terminal: "added", status: "changed" }); + + for (const [scope, dimension] of [ + ["process", "process"], + ["interaction", "interaction"], + ] as const) { + expect( + compareProcessCaptures( + { + ...capture, + residual_unknowns: [{ scope, reason: "observation was partial" }], + }, + capture, + ), + ).toMatchObject({ + status: "unknown", + [dimension]: "unknown", + terminal: "unchanged", + }); + } +}); + +it("compares raw terminal and process observations while honoring normalization shape", () => { + const capture = emptyCapture(); + expect( + compareProcessCaptures(capture, { + ...capture, + normalization: { + patterns: [], + time_bucket_ms: 10, + ports: true, + pids: true, + paths: true, + }, + }), + ).toMatchObject({ + status: "unchanged", + first_divergence: { status: "none" }, + }); + + const terminal = compareProcessCaptures( + { ...capture, frames: [{ sequence: 0, at_ms: 0, data: "bar" }] }, + { ...capture, frames: [{ sequence: 0, at_ms: 0, data: "foo\rbar" }] }, + ); + expect(terminal).toMatchObject({ + terminal: "changed", + first_divergence: { status: "found", dimension: "terminal" }, + }); + + expect( + compareProcessCaptures( + { + ...capture, + process_samples: [ + { + at_ms: 10, + pid: 1, + parent_pid: 0, + process_group_id: 1, + session_id: 1, + command: "worker", + }, + ], + }, + { + ...capture, + process_samples: [ + { + at_ms: 20, + pid: 1, + parent_pid: 2, + process_group_id: 1, + session_id: 1, + command: "worker", + }, + ], + }, + ), + ).toMatchObject({ process: "changed", status: "changed" }); +}); + +it("keeps filesystem comparison unknown without coverage and reports observed changes", () => { + const capture = emptyCapture(); + const incomplete = { + ...capture, + residual_unknowns: [ + { scope: "filesystem" as const, reason: "watcher unavailable" }, + ], + }; + expect(compareProcessCaptures(incomplete, incomplete)).toMatchObject({ + filesystem: "unknown", + status: "unknown", + }); + + const finalFile = { + path: "root_0:created.txt", + type: "file" as const, + mode: 0o644, + size: 4, + sha256: "a".repeat(64), + symlink_target: null, + }; + expect( + compareProcessCaptures(capture, { + ...capture, + filesystem_checkpoints: [ + { name: "before", at_ms: 0, files: [], effects: [], truncated: false }, + { + name: "after_settlement", + at_ms: 10, + files: [finalFile], + effects: [ + { + path: finalFile.path, + status: "created", + before: null, + after: finalFile, + }, + ], + truncated: false, + }, + ], + files_after: [finalFile], + filesystem_effects: [ + { + path: finalFile.path, + status: "created", + before: null, + after: finalFile, + }, + ], + }), + ).toMatchObject({ filesystem: "changed", status: "changed" }); +}); diff --git a/src/domain/processCapture.fixture.ts b/src/domain/process/processCapture.fixture.ts similarity index 100% rename from src/domain/processCapture.fixture.ts rename to src/domain/process/processCapture.fixture.ts diff --git a/src/domain/process/processCapture.ts b/src/domain/process/processCapture.ts new file mode 100644 index 0000000000..283706b125 --- /dev/null +++ b/src/domain/process/processCapture.ts @@ -0,0 +1,680 @@ +import { z } from "zod"; +import { jsonValueSchema } from "../jsonValue.js"; + +import { normalizationSchema } from "./processScenario.js"; +import { collectProcessCaptureIssues } from "./processCaptureValidation.js"; + +export * from "./processScenario.js"; + +/** Normalized raw PTY chunk, preserving transport-level output differences. */ +export interface TerminalFrame { + readonly sequence: number; + readonly at_ms: number; + readonly data: string; +} + +/** Serialized terminal state after interpreting control and resize sequences. */ +export interface RenderedTerminalFrame { + readonly sequence: number; + readonly at_ms: number; + readonly columns: number; + readonly rows: number; + readonly cursor_x: number; + readonly cursor_y: number; + readonly active_buffer: "normal" | "alternate"; + readonly lines: readonly string[]; + readonly serialized_state: string; +} + +/** Scheduled terminal interaction with its observed dispatch outcome. */ +export interface InteractionEvent { + readonly sequence: number; + readonly scheduled_at_ms: number; + readonly dispatched_at_ms: number; + readonly type: "input" | "resize" | "signal"; + readonly data: string; + readonly outcome: "dispatched" | "target_exited" | "failed"; +} + +/** One filesystem state used for before/after comparison. */ +interface FileStateIdentity { + readonly path: string; + readonly mode: number; + readonly size: number; +} + +export type FileState = FileStateIdentity & + ( + | { + readonly type: "file"; + readonly sha256: string | null; + readonly symlink_target: null; + } + | { + readonly type: "symlink"; + readonly sha256: null; + readonly symlink_target: string; + } + | { + readonly type: "directory" | "other"; + readonly sha256: null; + readonly symlink_target: null; + } + ); + +interface FileEffectIdentity { + readonly path: string; +} + +type FileEffect = FileEffectIdentity & + ( + | { + readonly status: "created"; + readonly before: null; + readonly after: FileState; + } + | { + readonly status: "deleted"; + readonly before: FileState; + readonly after: null; + } + | { + readonly status: "modified" | "unchanged"; + readonly before: FileState; + readonly after: FileState; + } + ); + +/** A sampled owned-process observation; sampling cannot prove syscall completeness. */ +export interface ProcessSample { + readonly at_ms: number; + readonly pid: number; + readonly parent_pid: number; + readonly command: string; + readonly process_group_id: number | null; + readonly session_id: number | null; +} + +/** Initial or final filesystem snapshot selected for observation. */ +export interface FilesystemCheckpoint { + readonly name: string; + readonly at_ms: number; + readonly files: readonly FileState[]; + readonly effects: readonly FileEffect[]; + readonly truncated: boolean; +} + +/** Capture collection whose members participate in global observation order. */ +export const PROCESS_CAPTURE_EVENT_COLLECTIONS = [ + "frames", + "rendered_frames", + "interaction_events", + "lifecycle", + "process_samples", + "filesystem_checkpoints", +] as const; + +export type ProcessCaptureEventCollection = + (typeof PROCESS_CAPTURE_EVENT_COLLECTIONS)[number]; + +/** One reference from global observation order into a capture collection. */ +export interface ProcessCaptureEventJournalEntry { + readonly capture_order: number; + readonly collection: ProcessCaptureEventCollection; + readonly index: number; +} + +/** Shared observation callback used by every process-capture producer. */ +export type RecordProcessCaptureEvent = ( + collection: ProcessCaptureEventCollection, + index: number, +) => void; + +/** Observed process-tree settlement and the cleanup required by that state. */ +export type ProcessSettlement = + | VerifiedProcessSettlement + | { + readonly state: "quiesced"; + readonly elapsed_ms: number; + readonly cleanup_outcome: "failed"; + }; + +export type VerifiedProcessSettlement = + | { + readonly state: "quiesced"; + readonly elapsed_ms: number; + readonly cleanup_outcome: "not_required"; + } + | { + readonly state: "alive_at_deadline" | "unverifiable"; + readonly elapsed_ms: number; + readonly cleanup_outcome: "cleaned" | "failed"; + }; + +/** + * Process capture observation set. + * + * `truncated` and `residual_unknowns` are semantic evidence: consumers must not + * infer equivalence from matching bounded observations when either is present. + */ +export interface UnverifiedProcessCapture { + readonly manifest: { + readonly rea_version: string; + readonly provider_version: string; + readonly platform: string; + readonly architecture: string; + readonly pty_backend: "node-pty"; + readonly started_at: string; + readonly completed_at: string; + readonly scenario: Readonly>; + readonly comparison_contract: Readonly>; + readonly full_scenario_sha256: string; + readonly comparison_contract_sha256: string; + /** Digest of the selected file sampled before spawn, when readable. */ + readonly selected_executable_sha256: string | null; + /** Digest associated with the launch only when path metadata stayed stable across spawn. */ + readonly executable_sha256: string | null; + /** + * Digest recorded by the original v3 format before selected and launch + * executable digests were distinguished. Kept only on migrated captures. + */ + readonly legacy_executable_sha256?: string | undefined; + readonly executable_identity: { + readonly state: "path_metadata_unchanged" | "unknown"; + readonly reason: string | null; + }; + readonly normalization_sha256: string; + }; + readonly normalization: z.infer; + readonly frames: readonly TerminalFrame[]; + readonly rendered_frames: readonly RenderedTerminalFrame[]; + readonly interaction_events: readonly InteractionEvent[]; + readonly exit: { + readonly code: number | null; + readonly signal: number | null; + readonly reason: "exited" | "timeout" | "idle_timeout"; + }; + readonly settlement: VerifiedProcessSettlement; + readonly process_samples: readonly ProcessSample[]; + readonly filesystem_checkpoints: readonly FilesystemCheckpoint[]; + /** + * Global observation order across independently recorded collections. + * + * Absence is accepted for captures written before this journal existed. + */ + readonly event_journal?: readonly ProcessCaptureEventJournalEntry[]; + readonly files_before: readonly FileState[]; + readonly files_after: readonly FileState[]; + readonly filesystem_effects: readonly FileEffect[]; + readonly truncated: boolean; + readonly limitations: readonly string[]; + readonly residual_unknowns: readonly { + readonly scope: + | "terminal" + | "interaction" + | "exit" + | "process" + | "filesystem" + | "cleanup" + | "network" + | "environment"; + readonly reason: string; + }[]; + readonly cleanup: { + readonly owned_process_group: "verified"; + readonly temporary_root: "removed"; + /** Unrelated processes whose ownership token the host could not expose. */ + readonly unverified_processes?: + | readonly UnverifiedCleanupProcess[] + | undefined; + }; +} + +const fileStateShape = { + path: z.string(), + mode: z.number().int().nonnegative(), + size: z.number().int().nonnegative(), +}; + +/** An unrelated process left untouched because its ownership is unknown. */ +export interface UnverifiedCleanupProcess { + readonly pid: number; + readonly reason: string; +} + +/** A resource cleanup result retained when observations cannot be verified. */ +export interface ProcessCaptureResourceCleanup { + readonly state: "cleaned" | "failed" | "unverified" | "not_required"; + readonly reason: string | null; + readonly unverified_processes?: + | readonly UnverifiedCleanupProcess[] + | undefined; +} + +export interface ProcessCaptureCleanupReport { + readonly owned_process_group: ProcessCaptureResourceCleanup; + readonly terminal_renderer: ProcessCaptureResourceCleanup; + readonly temporary_root: ProcessCaptureResourceCleanup; +} + +/** One observation field that may be unavailable when capture completion fails. */ +export type PartialProcessObservationField = + | { readonly state: "available"; readonly value: T } + | { readonly state: "unavailable"; readonly reason: string }; + +/** Snapshot facts available before filesystem comparison can be completed. */ +/** Snapshot facts available before filesystem comparison can be completed. */ +export interface PartialFilesystemSnapshot { + readonly files: readonly FileState[]; + readonly truncated: boolean; +} + +/** Available and unavailable fields when a run never becomes a full capture. */ +/** Available and unavailable fields when a run never becomes a full capture. */ +export interface IncompleteProcessCaptureObservations { + readonly target_pid: PartialProcessObservationField; + readonly frames: PartialProcessObservationField; + readonly rendered_frames: PartialProcessObservationField< + readonly RenderedTerminalFrame[] + >; + readonly interaction_events: PartialProcessObservationField< + readonly InteractionEvent[] + >; + readonly exit: PartialProcessObservationField<{ + readonly code: number | null; + readonly signal: number | null; + readonly reason: "exited" | "timeout" | "idle_timeout" | "cancelled"; + }>; + readonly settlement: PartialProcessObservationField<{ + readonly state: "quiesced" | "alive_at_deadline" | "unverifiable"; + readonly elapsed_ms: number; + }>; + readonly process_samples: PartialProcessObservationField< + readonly ProcessSample[] + >; + readonly filesystem_snapshots: { + readonly before: PartialProcessObservationField; + readonly after: PartialProcessObservationField; + }; + readonly event_journal: PartialProcessObservationField< + readonly ProcessCaptureEventJournalEntry[] + >; + readonly manifest: PartialProcessObservationField< + UnverifiedProcessCapture["manifest"] + >; +} + +const fileStateSchema = z.discriminatedUnion("type", [ + z.object({ + ...fileStateShape, + type: z.literal("file"), + sha256: z + .string() + .regex(/^[a-f0-9]{64}$/u) + .nullable(), + symlink_target: z.null(), + }), + z.object({ + ...fileStateShape, + type: z.literal("symlink"), + sha256: z.null(), + symlink_target: z.string(), + }), + z.object({ + ...fileStateShape, + type: z.enum(["directory", "other"]), + sha256: z.null(), + symlink_target: z.null(), + }), +]); +const fileEffectSchema = z.discriminatedUnion("status", [ + z.object({ + path: z.string(), + status: z.literal("created"), + before: z.null(), + after: fileStateSchema, + }), + z.object({ + path: z.string(), + status: z.literal("deleted"), + before: fileStateSchema, + after: z.null(), + }), + z.object({ + path: z.string(), + status: z.enum(["modified", "unchanged"]), + before: fileStateSchema, + after: fileStateSchema, + }), +]); +/** Exact serialized shape of a process capture. */ +const processSettlementSchema = z.discriminatedUnion("state", [ + z.object({ + state: z.literal("quiesced"), + elapsed_ms: z.number().int().nonnegative(), + cleanup_outcome: z.enum(["not_required", "failed"]), + }), + z.object({ + state: z.enum(["alive_at_deadline", "unverifiable"]), + elapsed_ms: z.number().int().nonnegative(), + cleanup_outcome: z.enum(["cleaned", "failed"]), + }), +]); + +const unverifiedCleanupProcessesSchema = z.array( + z.strictObject({ pid: z.number().int().positive(), reason: z.string() }), +); + +const processCaptureShapeSchema = z.strictObject({ + manifest: z.strictObject({ + rea_version: z.string().min(1), + provider_version: z.string().min(1), + platform: z.string().min(1), + architecture: z.string().min(1), + pty_backend: z.literal("node-pty"), + started_at: z.iso.datetime(), + completed_at: z.iso.datetime(), + scenario: z.record(z.string(), jsonValueSchema), + comparison_contract: z.record(z.string(), jsonValueSchema), + full_scenario_sha256: z.string().regex(/^[a-f0-9]{64}$/u), + comparison_contract_sha256: z.string().regex(/^[a-f0-9]{64}$/u), + selected_executable_sha256: z + .string() + .regex(/^[a-f0-9]{64}$/u) + .nullable(), + executable_sha256: z + .string() + .regex(/^[a-f0-9]{64}$/u) + .nullable(), + legacy_executable_sha256: z + .string() + .regex(/^[a-f0-9]{64}$/u) + .optional(), + executable_identity: z.strictObject({ + state: z.enum(["path_metadata_unchanged", "unknown"]), + reason: z.string().nullable(), + }), + normalization_sha256: z.string().regex(/^[a-f0-9]{64}$/u), + }), + normalization: normalizationSchema, + frames: z.array( + z.object({ + sequence: z.number().int().nonnegative(), + at_ms: z.number().int().nonnegative(), + data: z.string(), + }), + ), + rendered_frames: z.array( + z.object({ + sequence: z.number().int().nonnegative(), + at_ms: z.number().int().nonnegative(), + columns: z.number().int().positive(), + rows: z.number().int().positive(), + cursor_x: z.number().int().nonnegative(), + cursor_y: z.number().int().nonnegative(), + active_buffer: z.enum(["normal", "alternate"]), + lines: z.array(z.string()), + serialized_state: z.string(), + }), + ), + interaction_events: z.array( + z.object({ + sequence: z.number().int().nonnegative(), + scheduled_at_ms: z.number().int().nonnegative(), + dispatched_at_ms: z.number().int().nonnegative(), + type: z.enum(["input", "resize", "signal"]), + data: z.string(), + outcome: z.enum(["dispatched", "target_exited", "failed"]), + }), + ), + exit: z.object({ + code: z.number().int().nullable(), + signal: z.number().int().nullable(), + reason: z.enum(["exited", "timeout", "idle_timeout"]), + }), + settlement: z.discriminatedUnion("state", [ + z.object({ + state: z.literal("quiesced"), + elapsed_ms: z.number().int().nonnegative(), + cleanup_outcome: z.literal("not_required"), + }), + z.object({ + state: z.enum(["alive_at_deadline", "unverifiable"]), + elapsed_ms: z.number().int().nonnegative(), + cleanup_outcome: z.enum(["cleaned", "failed"]), + }), + ]), + process_samples: z.array( + z.object({ + at_ms: z.number().int().nonnegative(), + pid: z.number().int().positive(), + parent_pid: z.number().int().nonnegative(), + command: z.string(), + process_group_id: z.number().int().positive().nullable(), + session_id: z.number().int().nonnegative().nullable(), + }), + ), + filesystem_checkpoints: z.array( + z.object({ + name: z.enum(["before", "after_settlement"]), + at_ms: z.number().int().nonnegative(), + files: z.array(fileStateSchema), + effects: z.array(fileEffectSchema), + truncated: z.boolean(), + }), + ), + event_journal: z + .array( + z.object({ + capture_order: z.number().int().nonnegative(), + collection: z.enum(PROCESS_CAPTURE_EVENT_COLLECTIONS), + index: z.number().int().nonnegative(), + }), + ) + .default([]), + files_before: z.array(fileStateSchema), + files_after: z.array(fileStateSchema), + filesystem_effects: z.array(fileEffectSchema), + truncated: z.boolean(), + limitations: z.array(z.string()), + residual_unknowns: z.array( + z.object({ + scope: z.enum([ + "terminal", + "interaction", + "exit", + "process", + "filesystem", + "cleanup", + "network", + "environment", + ]), + reason: z.string(), + }), + ), + cleanup: z.object({ + owned_process_group: z.literal("verified"), + temporary_root: z.literal("removed"), + unverified_processes: unverifiedCleanupProcessesSchema.optional(), + }), +}); + +const processCleanupResourceSchema = z.strictObject({ + state: z.enum(["cleaned", "failed", "unverified", "not_required"]), + reason: z.string().nullable(), + unverified_processes: unverifiedCleanupProcessesSchema.optional(), +}); + +const partialObservationFieldSchema = ( + value: Schema, +) => + z.discriminatedUnion("state", [ + z.strictObject({ state: z.literal("available"), value }), + z.strictObject({ state: z.literal("unavailable"), reason: z.string() }), + ]); + +const incompleteProcessCaptureObservationsSchema = z.strictObject({ + target_pid: partialObservationFieldSchema(z.number().int().positive()), + frames: partialObservationFieldSchema(processCaptureShapeSchema.shape.frames), + rendered_frames: partialObservationFieldSchema( + processCaptureShapeSchema.shape.rendered_frames, + ), + interaction_events: partialObservationFieldSchema( + processCaptureShapeSchema.shape.interaction_events, + ), + exit: partialObservationFieldSchema( + z.strictObject({ + code: z.number().int().nullable(), + signal: z.number().int().nullable(), + reason: z.enum(["exited", "timeout", "idle_timeout", "cancelled"]), + }), + ), + settlement: partialObservationFieldSchema( + z.strictObject({ + state: z.enum(["quiesced", "alive_at_deadline", "unverifiable"]), + elapsed_ms: z.number().int().nonnegative(), + }), + ), + process_samples: partialObservationFieldSchema( + processCaptureShapeSchema.shape.process_samples, + ), + filesystem_snapshots: z.strictObject({ + before: partialObservationFieldSchema( + z.strictObject({ + files: z.array(fileStateSchema), + truncated: z.boolean(), + }), + ), + after: partialObservationFieldSchema( + z.strictObject({ + files: z.array(fileStateSchema), + truncated: z.boolean(), + }), + ), + }), + event_journal: partialObservationFieldSchema( + processCaptureShapeSchema.shape.event_journal, + ), + manifest: partialObservationFieldSchema( + processCaptureShapeSchema.shape.manifest, + ), +}); + +const partialCleanupReportSchema = z.strictObject({ + owned_process_group: processCleanupResourceSchema, + terminal_renderer: processCleanupResourceSchema, + temporary_root: processCleanupResourceSchema, +}); +const partialObservationMetadataSchema = z.strictObject({ + cleanup: partialCleanupReportSchema, + execution_failure: z.string().nullable(), +}); +const completedCaptureObservationSchema = z.strictObject({ + capture: processCaptureShapeSchema + .omit({ cleanup: true }) + .extend({ settlement: processSettlementSchema }), +}); +const incompleteCaptureObservationSchema = z.strictObject({ + observations: incompleteProcessCaptureObservationsSchema, +}); + +/** Validated, non-comparable process observations attached to cleanup errors. */ +export const partialProcessCaptureObservationSchema = z + .union([ + completedCaptureObservationSchema.extend( + partialObservationMetadataSchema.shape, + ), + incompleteCaptureObservationSchema.extend( + partialObservationMetadataSchema.shape, + ), + ]) + .superRefine((partial, context) => { + if ( + !Object.values(partial.cleanup).some( + ({ state }) => state === "failed" || state === "unverified", + ) + ) + context.addIssue({ + code: "custom", + path: ["cleanup"], + message: "partial observations require incomplete resource cleanup", + }); + + if ("observations" in partial) { + const { observations } = partial; + if ( + ![ + observations.frames, + observations.interaction_events, + observations.process_samples, + observations.filesystem_snapshots.before, + observations.event_journal, + ].some(({ state }) => state === "available") + ) + context.addIssue({ + code: "custom", + path: ["observations"], + message: "incomplete observations contain no collected evidence", + }); + return; + } + + const { capture } = partial; + const settlement = + capture.settlement.state === "quiesced" + ? { + ...capture.settlement, + cleanup_outcome: "not_required" as const, + } + : { ...capture.settlement, cleanup_outcome: "cleaned" as const }; + const validationCapture: UnverifiedProcessCapture = { + ...capture, + settlement, + cleanup: { + owned_process_group: "verified", + temporary_root: "removed", + }, + }; + for (const issue of collectProcessCaptureIssues(validationCapture)) + context.addIssue({ + code: "custom", + path: ["capture", ...issue.path.split(".")], + message: issue.message, + }); + }); + +/** One collected payload: completed capture data or explicit unavailable fields. */ +export type PartialProcessCaptureObservation = z.infer< + typeof partialProcessCaptureObservationSchema +>; + +/** Exact serialized shape plus all process-capture semantic invariants. */ +export const processCaptureSchema = processCaptureShapeSchema + .superRefine((capture, context) => { + if (capture.settlement.cleanup_outcome === "failed") + context.addIssue({ + code: "custom", + path: ["settlement", "cleanup_outcome"], + message: "verified captures cannot contain failed cleanup", + }); + for (const issue of collectProcessCaptureIssues(capture)) + context.addIssue({ + code: "custom", + path: issue.path.split("."), + message: issue.message, + }); + }) + .describe( + "The capture must preserve its canonical scenario, comparison, and normalization SHA-256 commitments; ordered capture timestamps and contiguous sequence numbers; before and final filesystem snapshots with truncation propagated; and exit-code consistency with deadline termination. When parsing older input without an event journal, REA supplies an empty journal; empty journals are valid. A non-empty journal must reference every captured observation exactly once with unique in-range references. These cross-field invariants are checked by REA after capture.", + ); + +export { parseProcessCapture } from "./processCaptureParsing.js"; +export type { ProcessCapture } from "./processCaptureParsing.js"; + +export { + compareProcessCaptures, + comparisonStatusSchema, + deriveProcessComparisonStatus, + PROCESS_COMPARISON_DIMENSIONS, + processCaptureComparisonSchema, +} from "./processComparison.js"; diff --git a/src/domain/processCapture.validation.test.ts b/src/domain/process/processCapture.validation.test.ts similarity index 65% rename from src/domain/processCapture.validation.test.ts rename to src/domain/process/processCapture.validation.test.ts index 0c7e899a8f..912b7c16d5 100644 --- a/src/domain/processCapture.validation.test.ts +++ b/src/domain/process/processCapture.validation.test.ts @@ -65,7 +65,7 @@ it("rejects altered v4 commitments and accepts canonical key reordering", () => executable_sha256: "f".repeat(64), }, }), - ).toThrow("executable_sha256"); + ).toThrow("executable_identity"); }); it("rejects settlement and cleanup combinations that cannot occur", () => { @@ -142,6 +142,92 @@ it("accepts old captures without a journal and validates complete journals", () } }); +it("migrates persisted v3 executable digests without claiming a selected digest", () => { + const capture = emptyCapture(); + const { + selected_executable_sha256: _selectedExecutableSha256, + executable_identity: _executableIdentity, + executable_sha256, + ...legacyManifest + } = capture.manifest; + const legacyCapture = { + ...capture, + manifest: { ...legacyManifest, executable_sha256 }, + }; + + const migrated = parseProcessCapture(legacyCapture); + + expect(migrated.manifest).toMatchObject({ + selected_executable_sha256: null, + executable_sha256: null, + executable_identity: { + state: "unknown", + reason: expect.stringContaining("did not distinguish"), + }, + legacy_executable_sha256: executable_sha256, + scenario: capture.manifest.scenario, + full_scenario_sha256: capture.manifest.full_scenario_sha256, + }); + expect(compareProcessCaptures(migrated, migrated)).toMatchObject({ + status: "unchanged", + }); + const exported = JSON.stringify(migrated); + expect(parseProcessCapture(JSON.parse(exported))).toEqual(migrated); + + expect(() => + parseProcessCapture({ + ...legacyCapture, + manifest: { + ...legacyCapture.manifest, + executable_sha256: "f".repeat(64), + }, + }), + ).toThrow("legacy_executable_sha256"); + + const nullDigestScenario = { + ...legacyCapture.manifest.scenario, + executable_sha256: null, + }; + const malformedLegacyBase = { + ...legacyCapture, + manifest: { + ...legacyCapture.manifest, + executable_sha256: null, + scenario: nullDigestScenario, + full_scenario_sha256: digestProcessCommitment(nullDigestScenario), + }, + }; + expect(() => parseProcessCapture(malformedLegacyBase)).toThrow(); + expect(() => + parseProcessCapture({ + ...malformedLegacyBase, + manifest: { + ...malformedLegacyBase.manifest, + executable_sha256: "not-a-digest", + }, + }), + ).toThrow(); + const { + executable_sha256: _missingLegacyDigest, + ...manifestWithoutLegacyDigest + } = malformedLegacyBase.manifest; + expect(() => + parseProcessCapture({ + ...malformedLegacyBase, + manifest: manifestWithoutLegacyDigest, + }), + ).toThrow(); + + const { executable_identity: _missingIdentity, ...incompleteModernManifest } = + capture.manifest; + expect(() => + parseProcessCapture({ + ...capture, + manifest: incompleteModernManifest, + }), + ).toThrow(); +}); + it("requires compatible contracts and enforces capture age through a clock seam", () => { const capture = emptyCapture(); expect(() => diff --git a/src/domain/processCaptureExample.ts b/src/domain/process/processCaptureExample.ts similarity index 89% rename from src/domain/processCaptureExample.ts rename to src/domain/process/processCaptureExample.ts index 72bc2e47f6..d920dabdc2 100644 --- a/src/domain/processCaptureExample.ts +++ b/src/domain/process/processCaptureExample.ts @@ -1,4 +1,4 @@ -import type { JsonValue } from "./jsonValue.js"; +import type { JsonValue } from "../jsonValue.js"; import { digestProcessCommitment } from "./processCapture.js"; const normalization = { @@ -25,7 +25,12 @@ export const EMPTY_PROCESS_CAPTURE_EXAMPLE = { comparison_contract: comparisonContract, full_scenario_sha256: digestProcessCommitment(scenario), comparison_contract_sha256: digestProcessCommitment(comparisonContract), + selected_executable_sha256: "0".repeat(64), executable_sha256: "0".repeat(64), + executable_identity: { + state: "path_metadata_unchanged", + reason: null, + }, normalization_sha256: digestProcessCommitment(normalization), }, normalization, diff --git a/src/domain/processCaptureParsing.ts b/src/domain/process/processCaptureParsing.ts similarity index 50% rename from src/domain/processCaptureParsing.ts rename to src/domain/process/processCaptureParsing.ts index dda070e590..018abf9669 100644 --- a/src/domain/processCaptureParsing.ts +++ b/src/domain/process/processCaptureParsing.ts @@ -2,6 +2,7 @@ import { processCaptureSchema, type UnverifiedProcessCapture, } from "./processCapture.js"; +import { z } from "zod"; declare class ProcessCaptureProof { private readonly verified: never; @@ -22,9 +23,47 @@ const attachProof = (capture: UnverifiedProcessCapture): ProcessCapture => { throw new TypeError("Process Capture proof ownership failed"); }; +const recordSchema = z.record(z.string(), z.unknown()); +const LEGACY_EXECUTABLE_IDENTITY_REASON = + "Legacy v3 capture did not distinguish selected and launch executable digests"; + +const migrateLegacyV3Capture = (input: unknown): unknown => { + const capture = recordSchema.safeParse(input); + if (!capture.success) return input; + const manifest = recordSchema.safeParse(capture.data.manifest); + if (!manifest.success) return input; + if ( + manifest.data.provider_version !== "3" || + "selected_executable_sha256" in manifest.data || + "executable_identity" in manifest.data || + "legacy_executable_sha256" in manifest.data + ) + return input; + + const { executable_sha256 } = manifest.data; + if ( + typeof executable_sha256 !== "string" || + !/^[a-f0-9]{64}$/u.test(executable_sha256) + ) + return input; + return { + ...capture.data, + manifest: { + ...manifest.data, + selected_executable_sha256: null, + executable_sha256: null, + executable_identity: { + state: "unknown", + reason: LEGACY_EXECUTABLE_IDENTITY_REASON, + }, + legacy_executable_sha256: executable_sha256, + }, + }; +}; + /** Parse unknown input and reject invalid commitments or semantics. */ export const parseProcessCapture = (input: unknown): ProcessCapture => { - const parsed = processCaptureSchema.safeParse(input); + const parsed = processCaptureSchema.safeParse(migrateLegacyV3Capture(input)); if (parsed.success) return attachProof(parsed.data); const issues = parsed.error.issues.flatMap((issue) => issue.code === "custom" diff --git a/src/domain/processCaptureValidation.ts b/src/domain/process/processCaptureValidation.ts similarity index 80% rename from src/domain/processCaptureValidation.ts rename to src/domain/process/processCaptureValidation.ts index 29be8c6e82..041c4c122c 100644 --- a/src/domain/processCaptureValidation.ts +++ b/src/domain/process/processCaptureValidation.ts @@ -26,8 +26,23 @@ const validateCommitments = ( require: RequireInvariant, ): void => { const { manifest } = capture; - require(manifest.scenario.executable_sha256 === - manifest.executable_sha256, "manifest.executable_sha256", "executable commitment does not match the scenario projection"); + if (manifest.legacy_executable_sha256 !== undefined) { + require(manifest.selected_executable_sha256 === null && + manifest.executable_sha256 === null && + manifest.executable_identity.state === "unknown" && + manifest.executable_identity.reason !== null && + manifest.scenario.executable_sha256 === + manifest.legacy_executable_sha256, "manifest.legacy_executable_sha256", "legacy executable digest must remain distinct from unknown selected and launch digests"); + } else { + require(manifest.scenario.executable_sha256 === + manifest.selected_executable_sha256, "manifest.selected_executable_sha256", "selected executable commitment does not match the scenario projection"); + } + require(manifest.executable_identity.state === "path_metadata_unchanged" + ? manifest.executable_sha256 === manifest.selected_executable_sha256 && + manifest.executable_identity.reason === null + : manifest.executable_sha256 === null && + manifest.executable_identity.reason !== + null, "manifest.executable_identity", "launch identity and executable digest do not agree"); for (const [field, value] of [ ["full_scenario_sha256", manifest.scenario], ["comparison_contract_sha256", manifest.comparison_contract], diff --git a/src/domain/processComparison.ts b/src/domain/process/processComparison.ts similarity index 99% rename from src/domain/processComparison.ts rename to src/domain/process/processComparison.ts index 7e69e0ae34..b6e5a8cc0a 100644 --- a/src/domain/processComparison.ts +++ b/src/domain/process/processComparison.ts @@ -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, diff --git a/src/domain/processEvidenceProvider.ts b/src/domain/process/processEvidenceProvider.ts similarity index 90% rename from src/domain/processEvidenceProvider.ts rename to src/domain/process/processEvidenceProvider.ts index 233d958919..cd05abe076 100644 --- a/src/domain/processEvidenceProvider.ts +++ b/src/domain/process/processEvidenceProvider.ts @@ -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 = { diff --git a/src/domain/processObservation.test.ts b/src/domain/process/processObservation.test.ts similarity index 79% rename from src/domain/processObservation.test.ts rename to src/domain/process/processObservation.test.ts index 7ebf55a25b..134938572a 100644 --- a/src/domain/processObservation.test.ts +++ b/src/domain/process/processObservation.test.ts @@ -5,10 +5,7 @@ import { processCaptureSchema, type UnverifiedProcessCapture, } from "./processCapture.js"; -import { - createProcessObservation, - projectProcessObservation, -} from "./processObservation.js"; +import { projectProcessObservation } from "./processObservation.js"; const emptyCapture = processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE); @@ -86,25 +83,7 @@ describe("process observation vocabulary", () => { expect(observations[4]?.captured_at_ms).toBeNull(); }); - it("uses the same constructor for live and post-hoc observations", () => { - const location = { - collection: "frames" as const, - index: 0, - capture_order: 4, - }; - const projected = projectProcessObservation(capture, location); - const live = createProcessObservation({ - source: "terminal_raw", - source_sequence: 0, - captured_at_ms: 1, - subject_id: null, - location, - payload: capture.frames[0], - }); - expect(live).toEqual(projected); - }); - - it("returns no observation for a missing raw record", () => { + it("returns no observation for a missing collection record", () => { expect( projectProcessObservation(capture, { collection: "frames", diff --git a/src/domain/processObservation.ts b/src/domain/process/processObservation.ts similarity index 98% rename from src/domain/processObservation.ts rename to src/domain/process/processObservation.ts index 95d6fc9e9c..167d36ec5f 100644 --- a/src/domain/processObservation.ts +++ b/src/domain/process/processObservation.ts @@ -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. */ diff --git a/src/domain/processScenario.test.ts b/src/domain/process/processScenario.test.ts similarity index 84% rename from src/domain/processScenario.test.ts rename to src/domain/process/processScenario.test.ts index a90eb59854..9f669328d3 100644 --- a/src/domain/processScenario.test.ts +++ b/src/domain/process/processScenario.test.ts @@ -61,6 +61,24 @@ describe("direct process scenarios", () => { ).toBe(true); }); + it("rejects NUL in strings passed to child-process APIs", () => { + const nul = "\0"; + for (const input of [ + { executable: `node${nul}` }, + { executable: "node", arguments: [nul] }, + { executable: "node", working_directory: `.${nul}` }, + { executable: "node", environment: { KEY: nul } }, + { executable: "node", filesystem_observation_paths: [nul] }, + ]) + expect(processScenarioSchema.safeParse(input).success).toBe(false); + expect( + processScenarioSchema.safeParse({ + executable: "node", + events: [{ type: "input", at_ms: 0, data: nul }], + }).success, + ).toBe(true); + }); + it("keeps bounded capture controls and ordered scheduled interaction", () => { const scenario = processScenarioSchema.parse({ executable: "node", diff --git a/src/domain/processScenario.ts b/src/domain/process/processScenario.ts similarity index 93% rename from src/domain/processScenario.ts rename to src/domain/process/processScenario.ts index b9924e0363..382cf3196d 100644 --- a/src/domain/processScenario.ts +++ b/src/domain/process/processScenario.ts @@ -14,6 +14,12 @@ const environmentName = z ? `${reservedRunIdEnvironmentName} is reserved by the process adapter` : "Environment names cannot contain '=' or NUL", }); +const childProcessString = z + .string() + .regex( + /^[^\0]*$/u, + "Values passed to operating-system APIs cannot contain NUL", + ); export const normalizationSchema = z.object({ paths: z.boolean(), pids: z.boolean(), @@ -86,11 +92,13 @@ export const processComparisonContract = ( */ export const processScenarioSchema = z .object({ - executable: z.string().min(1), - arguments: z.array(z.string()).default([]), - working_directory: z.string().default("."), - environment: z.record(environmentName, z.string()).default({}), - filesystem_observation_paths: z.array(z.string().min(1)).default([]), + executable: childProcessString.min(1), + arguments: z.array(childProcessString).default([]), + working_directory: childProcessString.default("."), + environment: z.record(environmentName, childProcessString).default({}), + filesystem_observation_paths: z + .array(childProcessString.min(1)) + .default([]), terminal: z .object({ columns: z.number().int().min(1).max(65_535).default(80), diff --git a/src/domain/processTraceAdapters.test.ts b/src/domain/process/processTraceAdapters.test.ts similarity index 100% rename from src/domain/processTraceAdapters.test.ts rename to src/domain/process/processTraceAdapters.test.ts diff --git a/src/domain/processTraceComparison.fixture.ts b/src/domain/process/processTraceComparison.fixture.ts similarity index 100% rename from src/domain/processTraceComparison.fixture.ts rename to src/domain/process/processTraceComparison.fixture.ts diff --git a/src/domain/processTraceComparison.test.ts b/src/domain/process/processTraceComparison.test.ts similarity index 100% rename from src/domain/processTraceComparison.test.ts rename to src/domain/process/processTraceComparison.test.ts diff --git a/src/domain/processTraceComparison.ts b/src/domain/process/processTraceComparison.ts similarity index 100% rename from src/domain/processTraceComparison.ts rename to src/domain/process/processTraceComparison.ts diff --git a/src/domain/processTraceDimensionProjection.ts b/src/domain/process/processTraceDimensionProjection.ts similarity index 100% rename from src/domain/processTraceDimensionProjection.ts rename to src/domain/process/processTraceDimensionProjection.ts diff --git a/src/domain/processTraceEvaluation.ts b/src/domain/process/processTraceEvaluation.ts similarity index 99% rename from src/domain/processTraceEvaluation.ts rename to src/domain/process/processTraceEvaluation.ts index bd4569ffca..5b61cb1c0b 100644 --- a/src/domain/processTraceEvaluation.ts +++ b/src/domain/process/processTraceEvaluation.ts @@ -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, diff --git a/src/domain/processTraceSpecification.test.ts b/src/domain/process/processTraceSpecification.test.ts similarity index 100% rename from src/domain/processTraceSpecification.test.ts rename to src/domain/process/processTraceSpecification.test.ts diff --git a/src/domain/processTraceSpecification.ts b/src/domain/process/processTraceSpecification.ts similarity index 99% rename from src/domain/processTraceSpecification.ts rename to src/domain/process/processTraceSpecification.ts index 75e129e477..5c70dc4a2f 100644 --- a/src/domain/processTraceSpecification.ts +++ b/src/domain/process/processTraceSpecification.ts @@ -1,7 +1,7 @@ import canonicalize from "canonicalize"; import { z } from "zod"; -import { jsonValueSchema } from "./jsonValue.js"; +import { jsonValueSchema } from "../jsonValue.js"; import { comparableProcessObservationPayload, processObservationSourceSchema, diff --git a/src/domain/processCapture.comparison.test.ts b/src/domain/processCapture.comparison.test.ts deleted file mode 100644 index 431fef24e7..0000000000 --- a/src/domain/processCapture.comparison.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { expect, it } from "vitest"; - -import { - compareUnverifiedProcessCaptures as compareProcessCaptures, - emptyProcessCapture as emptyCapture, -} from "./processCapture.fixture.js"; - -it("classifies missing observations as unknown and one-sided evidence as added", () => { - const base = { - manifest: emptyCapture().manifest, - settlement: emptyCapture().settlement, - normalization: { - paths: true, - pids: true, - ports: true, - time_bucket_ms: 10, - patterns: [], - }, - frames: [], - rendered_frames: [], - interaction_events: [], - exit: { code: 0, signal: null, reason: "exited" as const }, - process_samples: [], - filesystem_checkpoints: emptyCapture().filesystem_checkpoints, - files_before: [], - files_after: [], - filesystem_effects: [], - truncated: false, - limitations: [], - residual_unknowns: [], - cleanup: { - owned_process_group: "verified" as const, - temporary_root: "removed" as const, - }, - }; - const added = compareProcessCaptures(base, { - ...base, - frames: [{ sequence: 0, at_ms: 0, data: "new" }], - rendered_frames: [ - { - sequence: 0, - at_ms: 0, - columns: 3, - rows: 1, - cursor_x: 3, - cursor_y: 0, - active_buffer: "normal", - lines: ["new"], - serialized_state: "new", - }, - ], - }); - expect(added.terminal).toBe("added"); - expect(added.status).toBe("changed"); - const unknown = compareProcessCaptures( - { ...base, residual_unknowns: [{ scope: "process", reason: "sampled" }] }, - base, - ); - expect(unknown.process).toBe("unknown"); - expect(unknown.status).toBe("unknown"); -}); - -it("treats equivalent normalization records as equal regardless of member order", () => { - const capture = emptyCapture(); - - expect( - compareProcessCaptures(capture, { - ...capture, - normalization: { - patterns: [], - time_bucket_ms: 10, - ports: true, - pids: true, - paths: true, - }, - }), - ).toMatchObject({ - status: "unchanged", - first_divergence: { status: "none" }, - }); -}); - -it("compares raw terminal chunks even when rendered states agree", () => { - const capture = { - manifest: emptyCapture().manifest, - settlement: emptyCapture().settlement, - normalization: { - paths: true, - pids: true, - ports: true, - time_bucket_ms: 10, - patterns: [], - }, - frames: [{ sequence: 0, at_ms: 0, data: "bar" }], - rendered_frames: [], - interaction_events: [], - exit: { code: 0, signal: null, reason: "exited" as const }, - process_samples: [], - filesystem_checkpoints: emptyCapture().filesystem_checkpoints, - files_before: [], - files_after: [], - filesystem_effects: [], - truncated: false, - limitations: [], - residual_unknowns: [], - cleanup: { - owned_process_group: "verified" as const, - temporary_root: "removed" as const, - }, - }; - - const comparison = compareProcessCaptures(capture, { - ...capture, - frames: [{ sequence: 0, at_ms: 0, data: "foo\rbar" }], - }); - expect(comparison.terminal).toBe("changed"); - expect(comparison.first_divergence).toMatchObject({ - status: "found", - dimension: "terminal", - }); -}); - -it("keeps filesystem evidence unknown when stable snapshots match", () => { - const capture = { - manifest: emptyCapture().manifest, - settlement: emptyCapture().settlement, - normalization: { - paths: true, - pids: true, - ports: true, - time_bucket_ms: 10, - patterns: [], - }, - frames: [], - rendered_frames: [], - interaction_events: [], - exit: { code: 0, signal: null, reason: "exited" as const }, - process_samples: [], - filesystem_checkpoints: emptyCapture().filesystem_checkpoints, - files_before: [], - files_after: [], - filesystem_effects: [], - truncated: false, - limitations: [], - residual_unknowns: [ - { scope: "filesystem" as const, reason: "watcher unavailable" }, - ], - cleanup: { - owned_process_group: "verified" as const, - temporary_root: "removed" as const, - }, - }; - - const comparison = compareProcessCaptures(capture, capture); - expect(comparison.filesystem).toBe("unknown"); - expect(comparison.status).toBe("unknown"); - - const complete = { ...capture, residual_unknowns: [] }; - const finalFile = { - path: "root_0:created.txt", - type: "file" as const, - mode: 0o644, - size: 4, - sha256: "a".repeat(64), - symlink_target: null, - }; - const changed = compareProcessCaptures(complete, { - ...complete, - filesystem_checkpoints: [ - { name: "before", at_ms: 0, files: [], effects: [], truncated: false }, - { - name: "after_settlement", - at_ms: 10, - files: [finalFile], - effects: [ - { - path: finalFile.path, - status: "created", - before: null, - after: finalFile, - }, - ], - truncated: false, - }, - ], - files_after: [finalFile], - filesystem_effects: [ - { - path: finalFile.path, - status: "created", - before: null, - after: finalFile, - }, - ], - }); - expect(changed.filesystem).toBe("changed"); - expect(changed.status).toBe("changed"); -}); diff --git a/src/domain/processCapture.traceProjection.test.ts b/src/domain/processCapture.traceProjection.test.ts deleted file mode 100644 index b6b77917ae..0000000000 --- a/src/domain/processCapture.traceProjection.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect, it } from "vitest"; - -import { - compareUnverifiedProcessCaptures as compareProcessCaptures, - emptyProcessCapture as emptyCapture, -} from "./processCapture.fixture.js"; - -it("detects changes in normalized process sample metadata", () => { - const capture = { - manifest: emptyCapture().manifest, - settlement: emptyCapture().settlement, - normalization: { - paths: true, - pids: true, - ports: true, - time_bucket_ms: 10, - patterns: [], - }, - frames: [], - rendered_frames: [], - interaction_events: [], - exit: { code: 0, signal: null, reason: "exited" as const }, - process_samples: [ - { - at_ms: 10, - pid: 1, - parent_pid: 0, - process_group_id: 1, - session_id: 1, - command: "worker", - }, - ], - filesystem_checkpoints: emptyCapture().filesystem_checkpoints, - files_before: [], - files_after: [], - filesystem_effects: [], - truncated: false, - limitations: [], - residual_unknowns: [], - cleanup: { - owned_process_group: "verified" as const, - temporary_root: "removed" as const, - }, - }; - const changed = { - ...capture, - process_samples: [ - { - at_ms: 20, - pid: 1, - parent_pid: 2, - process_group_id: 1, - session_id: 1, - command: "worker", - }, - ], - }; - - const comparison = compareProcessCaptures(capture, changed); - expect(comparison.process).toBe("changed"); - expect(comparison.status).toBe("changed"); -}); diff --git a/src/domain/processCapture.ts b/src/domain/processCapture.ts deleted file mode 100644 index 76d888c704..0000000000 --- a/src/domain/processCapture.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { z } from "zod"; -import { jsonValueSchema } from "./jsonValue.js"; - -import { normalizationSchema } from "./processScenario.js"; -import { collectProcessCaptureIssues } from "./processCaptureValidation.js"; - -export * from "./processScenario.js"; - -/** Normalized raw PTY chunk, preserving transport-level output differences. */ -export interface TerminalFrame { - readonly sequence: number; - readonly at_ms: number; - readonly data: string; -} - -/** Serialized terminal state after interpreting control and resize sequences. */ -export interface RenderedTerminalFrame { - readonly sequence: number; - readonly at_ms: number; - readonly columns: number; - readonly rows: number; - readonly cursor_x: number; - readonly cursor_y: number; - readonly active_buffer: "normal" | "alternate"; - readonly lines: readonly string[]; - readonly serialized_state: string; -} - -/** Scheduled terminal interaction with its observed dispatch outcome. */ -export interface InteractionEvent { - readonly sequence: number; - readonly scheduled_at_ms: number; - readonly dispatched_at_ms: number; - readonly type: "input" | "resize" | "signal"; - readonly data: string; - readonly outcome: "dispatched" | "target_exited" | "failed"; -} - -/** One filesystem state used for before/after comparison. */ -interface FileStateIdentity { - readonly path: string; - readonly mode: number; - readonly size: number; -} - -export type FileState = FileStateIdentity & - ( - | { - readonly type: "file"; - readonly sha256: string | null; - readonly symlink_target: null; - } - | { - readonly type: "symlink"; - readonly sha256: null; - readonly symlink_target: string; - } - | { - readonly type: "directory" | "other"; - readonly sha256: null; - readonly symlink_target: null; - } - ); - -interface FileEffectIdentity { - readonly path: string; -} - -type FileEffect = FileEffectIdentity & - ( - | { - readonly status: "created"; - readonly before: null; - readonly after: FileState; - } - | { - readonly status: "deleted"; - readonly before: FileState; - readonly after: null; - } - | { - readonly status: "modified" | "unchanged"; - readonly before: FileState; - readonly after: FileState; - } - ); - -/** A sampled owned-process observation; sampling cannot prove syscall completeness. */ -export interface ProcessSample { - readonly at_ms: number; - readonly pid: number; - readonly parent_pid: number; - readonly command: string; - readonly process_group_id: number | null; - readonly session_id: number | null; -} - -/** Initial or final filesystem snapshot selected for observation. */ -export interface FilesystemCheckpoint { - readonly name: string; - readonly at_ms: number; - readonly files: readonly FileState[]; - readonly effects: readonly FileEffect[]; - readonly truncated: boolean; -} - -/** Capture collection whose members participate in global observation order. */ -export const PROCESS_CAPTURE_EVENT_COLLECTIONS = [ - "frames", - "rendered_frames", - "interaction_events", - "lifecycle", - "process_samples", - "filesystem_checkpoints", -] as const; - -export type ProcessCaptureEventCollection = - (typeof PROCESS_CAPTURE_EVENT_COLLECTIONS)[number]; - -/** One reference from global observation order into a capture collection. */ -export interface ProcessCaptureEventJournalEntry { - readonly capture_order: number; - readonly collection: ProcessCaptureEventCollection; - readonly index: number; -} - -/** Shared observation callback used by every process-capture producer. */ -export type RecordProcessCaptureEvent = ( - collection: ProcessCaptureEventCollection, - index: number, -) => void; - -/** Observed process-tree settlement and the cleanup required by that state. */ -export type ProcessSettlement = - | { - readonly state: "quiesced"; - readonly elapsed_ms: number; - readonly cleanup_outcome: "not_required"; - } - | { - readonly state: "alive_at_deadline" | "unverifiable"; - readonly elapsed_ms: number; - readonly cleanup_outcome: "cleaned" | "failed"; - }; - -/** - * Process capture observation set. - * - * `truncated` and `residual_unknowns` are semantic evidence: consumers must not - * infer equivalence from matching bounded observations when either is present. - */ -export interface UnverifiedProcessCapture { - readonly manifest: { - readonly rea_version: string; - readonly provider_version: string; - readonly platform: string; - readonly architecture: string; - readonly pty_backend: "node-pty"; - readonly started_at: string; - readonly completed_at: string; - readonly scenario: Readonly>; - readonly comparison_contract: Readonly>; - readonly full_scenario_sha256: string; - readonly comparison_contract_sha256: string; - readonly executable_sha256: string; - readonly normalization_sha256: string; - }; - readonly normalization: z.infer; - readonly frames: readonly TerminalFrame[]; - readonly rendered_frames: readonly RenderedTerminalFrame[]; - readonly interaction_events: readonly InteractionEvent[]; - readonly exit: { - readonly code: number | null; - readonly signal: number | null; - readonly reason: "exited" | "timeout" | "idle_timeout"; - }; - readonly settlement: ProcessSettlement; - readonly process_samples: readonly ProcessSample[]; - readonly filesystem_checkpoints: readonly FilesystemCheckpoint[]; - /** - * Global observation order across independently recorded collections. - * - * Absence is accepted for captures written before this journal existed. - */ - readonly event_journal?: readonly ProcessCaptureEventJournalEntry[]; - readonly files_before: readonly FileState[]; - readonly files_after: readonly FileState[]; - readonly filesystem_effects: readonly FileEffect[]; - readonly truncated: boolean; - readonly limitations: readonly string[]; - readonly residual_unknowns: readonly { - readonly scope: - | "terminal" - | "interaction" - | "exit" - | "process" - | "filesystem" - | "cleanup" - | "network" - | "environment"; - readonly reason: string; - }[]; - readonly cleanup: { - readonly owned_process_group: "verified"; - readonly temporary_root: "removed"; - }; -} - -const fileStateShape = { - path: z.string(), - mode: z.number().int().nonnegative(), - size: z.number().int().nonnegative(), -}; -const fileStateSchema = z.discriminatedUnion("type", [ - z.object({ - ...fileStateShape, - type: z.literal("file"), - sha256: z - .string() - .regex(/^[a-f0-9]{64}$/u) - .nullable(), - symlink_target: z.null(), - }), - z.object({ - ...fileStateShape, - type: z.literal("symlink"), - sha256: z.null(), - symlink_target: z.string(), - }), - z.object({ - ...fileStateShape, - type: z.enum(["directory", "other"]), - sha256: z.null(), - symlink_target: z.null(), - }), -]); -const fileEffectSchema = z.discriminatedUnion("status", [ - z.object({ - path: z.string(), - status: z.literal("created"), - before: z.null(), - after: fileStateSchema, - }), - z.object({ - path: z.string(), - status: z.literal("deleted"), - before: fileStateSchema, - after: z.null(), - }), - z.object({ - path: z.string(), - status: z.enum(["modified", "unchanged"]), - before: fileStateSchema, - after: fileStateSchema, - }), -]); -/** Exact serialized shape of a process capture. */ -const processCaptureShapeSchema: z.ZodType = - z.strictObject({ - manifest: z.strictObject({ - rea_version: z.string().min(1), - provider_version: z.string().min(1), - platform: z.string().min(1), - architecture: z.string().min(1), - pty_backend: z.literal("node-pty"), - started_at: z.iso.datetime(), - completed_at: z.iso.datetime(), - scenario: z.record(z.string(), jsonValueSchema), - comparison_contract: z.record(z.string(), jsonValueSchema), - full_scenario_sha256: z.string().regex(/^[a-f0-9]{64}$/u), - comparison_contract_sha256: z.string().regex(/^[a-f0-9]{64}$/u), - executable_sha256: z.string().regex(/^[a-f0-9]{64}$/u), - normalization_sha256: z.string().regex(/^[a-f0-9]{64}$/u), - }), - normalization: normalizationSchema, - frames: z.array( - z.object({ - sequence: z.number().int().nonnegative(), - at_ms: z.number().int().nonnegative(), - data: z.string(), - }), - ), - rendered_frames: z.array( - z.object({ - sequence: z.number().int().nonnegative(), - at_ms: z.number().int().nonnegative(), - columns: z.number().int().positive(), - rows: z.number().int().positive(), - cursor_x: z.number().int().nonnegative(), - cursor_y: z.number().int().nonnegative(), - active_buffer: z.enum(["normal", "alternate"]), - lines: z.array(z.string()), - serialized_state: z.string(), - }), - ), - interaction_events: z.array( - z.object({ - sequence: z.number().int().nonnegative(), - scheduled_at_ms: z.number().int().nonnegative(), - dispatched_at_ms: z.number().int().nonnegative(), - type: z.enum(["input", "resize", "signal"]), - data: z.string(), - outcome: z.enum(["dispatched", "target_exited", "failed"]), - }), - ), - exit: z.object({ - code: z.number().int().nullable(), - signal: z.number().int().nullable(), - reason: z.enum(["exited", "timeout", "idle_timeout"]), - }), - settlement: z.discriminatedUnion("state", [ - z.object({ - state: z.literal("quiesced"), - elapsed_ms: z.number().int().nonnegative(), - cleanup_outcome: z.literal("not_required"), - }), - z.object({ - state: z.enum(["alive_at_deadline", "unverifiable"]), - elapsed_ms: z.number().int().nonnegative(), - cleanup_outcome: z.enum(["cleaned", "failed"]), - }), - ]), - process_samples: z.array( - z.object({ - at_ms: z.number().int().nonnegative(), - pid: z.number().int().positive(), - parent_pid: z.number().int().nonnegative(), - command: z.string(), - process_group_id: z.number().int().positive().nullable(), - session_id: z.number().int().nonnegative().nullable(), - }), - ), - filesystem_checkpoints: z.array( - z.object({ - name: z.enum(["before", "after_settlement"]), - at_ms: z.number().int().nonnegative(), - files: z.array(fileStateSchema), - effects: z.array(fileEffectSchema), - truncated: z.boolean(), - }), - ), - event_journal: z - .array( - z.object({ - capture_order: z.number().int().nonnegative(), - collection: z.enum(PROCESS_CAPTURE_EVENT_COLLECTIONS), - index: z.number().int().nonnegative(), - }), - ) - .default([]), - files_before: z.array(fileStateSchema), - files_after: z.array(fileStateSchema), - filesystem_effects: z.array(fileEffectSchema), - truncated: z.boolean(), - limitations: z.array(z.string()), - residual_unknowns: z.array( - z.object({ - scope: z.enum([ - "terminal", - "interaction", - "exit", - "process", - "filesystem", - "cleanup", - "network", - "environment", - ]), - reason: z.string(), - }), - ), - cleanup: z.object({ - owned_process_group: z.literal("verified"), - temporary_root: z.literal("removed"), - }), - }); - -/** Exact serialized shape plus all process-capture semantic invariants. */ -export const processCaptureSchema = processCaptureShapeSchema.superRefine( - (capture, context) => { - for (const issue of collectProcessCaptureIssues(capture)) - context.addIssue({ - code: "custom", - path: issue.path.split("."), - message: issue.message, - }); - }, -); - -export { parseProcessCapture } from "./processCaptureParsing.js"; -export type { ProcessCapture } from "./processCaptureParsing.js"; - -export { - compareProcessCaptures, - comparisonStatusSchema, - deriveProcessComparisonStatus, - PROCESS_COMPARISON_DIMENSIONS, - processCaptureComparisonSchema, -} from "./processComparison.js"; diff --git a/src/domain/processEvidenceProvider.test.ts b/src/domain/processEvidenceProvider.test.ts deleted file mode 100644 index 637dd36e67..0000000000 --- a/src/domain/processEvidenceProvider.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect, it } from "vitest"; - -import { - PROCESS_PROVIDER, - isProcessEvidenceProvider, -} from "./processEvidenceProvider.js"; - -it.each(["REA process capture", "REA deterministic process harness"])( - "accepts the exact v3 process provider name %s", - (name) => { - expect(isProcessEvidenceProvider({ ...PROCESS_PROVIDER, name })).toBe(true); - }, -); - -it.each([ - { name: "REA process capture extra" }, - { name: "REA deterministic process harness extra" }, - { name: "rea process capture" }, - { name: "REA Process Capture" }, - { name: " REA process capture" }, - { name: "REA process capture " }, - { name: "REA deterministic process harness " }, - { name: "Fixture process provider" }, - { id: "fixture-process" }, - { version: "2" }, - { version: "4" }, - { version: null }, -])("rejects other process identities: %j", (change) => { - expect(isProcessEvidenceProvider({ ...PROCESS_PROVIDER, ...change })).toBe( - false, - ); -}); diff --git a/src/domain/protocolCapture.test.ts b/src/domain/protocolCapture.test.ts deleted file mode 100644 index ba58189545..0000000000 --- a/src/domain/protocolCapture.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { describe, expect, it } from "vitest"; - -import { - classifyProtocolFamily, - decodeGrpcFrame, - decodeJsonRpc, - decodeMessagePack, - inferMessageSchema, - protocolCaptureSchema, - type ProtocolMessage, -} from "./protocolCapture.js"; - -describe("protocol capture", () => { - it("accepts every captured message", () => { - const message: ProtocolMessage = { - sequence: 0, - at_ms: 0, - family: "json-rpc", - direction: "request", - endpoint: null, - content_type: null, - raw_payload: null, - decoded_fields: [], - schema_hypotheses: [], - truncated: false, - credentials_redacted: false, - }; - - expect( - protocolCaptureSchema.parse({ - family: "json-rpc", - messages: Array.from({ length: 10_001 }, (_, sequence) => ({ - ...message, - sequence, - })), - }).messages, - ).toHaveLength(10_001); - }); - - it("classifies gRPC from content type", () => { - expect(classifyProtocolFamily("application/grpc+proto", null)).toBe("grpc"); - }); - - it("classifies protobuf from content type", () => { - expect(classifyProtocolFamily("application/x-protobuf", null)).toBe( - "protobuf", - ); - }); - - it("classifies json-rpc from content type", () => { - expect(classifyProtocolFamily("application/json-rpc", null)).toBe( - "json-rpc", - ); - }); - - it("classifies msgpack from content type", () => { - expect(classifyProtocolFamily("application/msgpack", null)).toBe( - "messagepack", - ); - }); - - it("classifies from path when content type is null", () => { - expect(classifyProtocolFamily(null, "/some/grpc/method")).toBe("grpc"); - expect(classifyProtocolFamily(null, "/api/json-rpc/2.0")).toBe("json-rpc"); - }); - - it("returns null for unknown protocols", () => { - expect(classifyProtocolFamily("text/plain", null)).toBeNull(); - expect(classifyProtocolFamily(null, null)).toBeNull(); - }); -}); - -describe("JSON-RPC decoding", () => { - it("decodes the captured MCP SDK initialize request with exact field values", async () => { - const payload = await readFile( - new URL( - "../../tests/fixtures/golden/mcp-initialize.json", - import.meta.url, - ), - ); - expect(decodeJsonRpc(payload)).toEqual({ - decoded_fields: [ - { - path: "method", - wire_type: "json", - value: "initialize", - inferred: false, - }, - { - path: "params", - wire_type: "json", - inferred: false, - value: { - protocolVersion: "2025-11-25", - capabilities: {}, - clientInfo: { name: "rea-protocol-golden", version: "1" }, - }, - }, - { path: "jsonrpc", wire_type: "json", value: "2.0", inferred: false }, - { path: "id", wire_type: "json", value: 0, inferred: false }, - ], - truncated: false, - }); - }); - - it("preserves decoded fields whose names resemble credentials", () => { - const payload = new TextEncoder().encode( - JSON.stringify({ - password: "observed-value", - authorization: { scheme: "custom", value: "observed-header" }, - username: "admin", - }), - ); - const result = decodeJsonRpc(payload); - expect(result.decoded_fields).toEqual([ - { - path: "password", - wire_type: "json", - value: "observed-value", - inferred: false, - }, - { - path: "authorization", - wire_type: "json", - value: { scheme: "custom", value: "observed-header" }, - inferred: false, - }, - { - path: "username", - wire_type: "json", - value: "admin", - inferred: false, - }, - ]); - }); -}); - -describe("MessagePack decoding", () => { - it("does not report unobserved payload bytes as truncation", () => { - const result = decodeMessagePack(new Uint8Array(10_001).fill(0x82)); - - expect(result.truncated).toBe(false); - }); - - it("detects map type", () => { - const payload = new Uint8Array([0x82]); - const result = decodeMessagePack(payload); - expect(result.decoded_fields[0]?.value).toBe("map"); - }); - - it("detects array type", () => { - const payload = new Uint8Array([0x92]); - const result = decodeMessagePack(payload); - expect(result.decoded_fields[0]?.value).toBe("array"); - }); - - it("reports unknown type for other tags", () => { - const payload = new Uint8Array([0xff]); - const result = decodeMessagePack(payload); - expect(result.decoded_fields[0]?.value).toBe("unknown"); - expect(result.decoded_fields[0]?.inferred).toBe(true); - }); -}); - -describe("gRPC frame decoding", () => { - it("decodes a valid gRPC frame header", () => { - const payload = new Uint8Array([ - 0x00, 0x00, 0x00, 0x00, 0x05, 1, 2, 3, 4, 5, - ]); - const result = decodeGrpcFrame(payload); - expect(result.decoded_fields).toHaveLength(2); - expect(result.decoded_fields[0]?.path).toBe("compressed"); - expect(result.decoded_fields[0]?.value).toBe(false); - expect(result.decoded_fields[1]?.path).toBe("message_length"); - expect(result.decoded_fields[1]?.value).toBe(5); - }); - - it("detects compressed flag", () => { - const payload = new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x0a]); - const result = decodeGrpcFrame(payload); - expect(result.decoded_fields[0]?.value).toBe(true); - }); - - it("handles too-short frames", () => { - const payload = new Uint8Array([0x00, 0x01]); - const result = decodeGrpcFrame(payload); - expect(result.decoded_fields[0]?.path).toBe("error"); - }); -}); - -describe("schema inference", () => { - it("infers schema from repeated messages", () => { - const messages: ProtocolMessage[] = [ - { - sequence: 0, - at_ms: 0, - family: "json-rpc", - direction: "request", - endpoint: "/api", - content_type: "application/json", - raw_payload: null, - decoded_fields: [ - { path: "method", wire_type: "json", value: "add", inferred: false }, - { path: "id", wire_type: "json", value: 1, inferred: false }, - ], - schema_hypotheses: [], - truncated: false, - credentials_redacted: false, - }, - { - sequence: 1, - at_ms: 10, - family: "json-rpc", - direction: "response", - endpoint: "/api", - content_type: "application/json", - raw_payload: null, - decoded_fields: [ - { path: "result", wire_type: "json", value: 3, inferred: false }, - { path: "id", wire_type: "json", value: 1, inferred: false }, - ], - schema_hypotheses: [], - truncated: false, - credentials_redacted: false, - }, - ]; - const hypotheses = inferMessageSchema(messages); - expect(hypotheses).toHaveLength(3); - const idHypothesis = hypotheses.find((h) => h.field_name === "id"); - expect(idHypothesis).toBeDefined(); - expect(idHypothesis!.evidence_count).toBe(2); - expect(idHypothesis!.confidence).toBe(1); - }); -}); - -describe("protocol capture schema validation", () => { - it("rejects a capture with an unknown protocol family", () => { - const result = protocolCaptureSchema.safeParse({ - family: "smtp", - messages: [], - inferred_schema: [], - has_truncated: false, - credentials_detected: false, - }); - expect(result.success).toBe(false); - }); - - it("rejects a capture that declares a non-boolean truncation flag", () => { - const result = protocolCaptureSchema.safeParse({ - family: "grpc", - messages: [], - inferred_schema: [], - has_truncated: "no", - credentials_detected: false, - }); - expect(result.success).toBe(false); - }); -}); diff --git a/src/domain/protocolCapture.ts b/src/domain/protocolCapture.ts deleted file mode 100644 index dbb60bedbb..0000000000 --- a/src/domain/protocolCapture.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { z } from "zod"; - -import { jsonValueSchema } from "./jsonValue.js"; - -/** Supported protocol families for structured protocol capture. */ -export const protocolFamilySchema = z.enum([ - "grpc", - "protobuf", - "json-rpc", - "messagepack", -]); -export type ProtocolFamily = z.infer; - -/** A decoded field from a protocol message. */ -export const decodedFieldSchema = z.strictObject({ - /** Field name or path within the message structure. */ - path: z.string().min(1), - /** Wire type or encoding hint. */ - wire_type: z.string().nullable(), - /** Decoded field value. */ - value: jsonValueSchema.nullable(), - /** Whether this field value was inferred rather than decoded. */ - inferred: z.boolean().default(false), -}); -export type DecodedField = z.infer; - -/** Schema inference hypothesis for a repeated message pattern. */ -export const schemaHypothesisSchema = z.strictObject({ - /** Hypothesized field name. */ - field_name: z.string().min(1), - /** Inferred type. */ - inferred_type: z.string().min(1), - /** Confidence: 0-1 */ - confidence: z.number().min(0).max(1), - /** Supporting evidence count. */ - evidence_count: z.number().int().nonnegative(), -}); -export type SchemaHypothesis = z.infer; - -/** Result of decoding a single protocol message. */ -export const protocolMessageSchema = z.strictObject({ - /** Monotonic sequence number. */ - sequence: z.number().int().nonnegative(), - /** Timestamp in milliseconds. */ - at_ms: z.number().int().nonnegative(), - /** Protocol family. */ - family: protocolFamilySchema, - /** Direction of the message. */ - direction: z.enum(["request", "response"]), - /** Transport endpoint or path (e.g. gRPC method path). */ - endpoint: z.string().nullable(), - /** Content type of the raw payload. */ - content_type: z.string().nullable(), - /** Raw payload bytes as base64. */ - raw_payload: z.string().nullable(), - /** Decoded fields from the payload. */ - decoded_fields: z.array(decodedFieldSchema).default([]), - /** Inferred schema hypotheses. */ - schema_hypotheses: z.array(schemaHypothesisSchema).default([]), - /** Whether decoding was truncated. */ - truncated: z.boolean().default(false), - /** Whether the capture producer marked credentials as redacted. */ - credentials_redacted: z.boolean().default(false), -}); -export type ProtocolMessage = z.infer; - -/** A captured protocol session with messages and inferred schemas. */ -export const protocolCaptureSchema = z.strictObject({ - /** Protocol family for this capture. */ - family: protocolFamilySchema, - /** Captured messages in order. */ - messages: z.array(protocolMessageSchema).min(0), - /** Aggregated schema hypotheses across all messages. */ - inferred_schema: z.array(schemaHypothesisSchema).default([]), - /** Whether any message was truncated. */ - has_truncated: z.boolean().default(false), - /** Whether the capture producer marked credential content as redacted. */ - credentials_detected: z.boolean().default(false), -}); - -/** Classification of a byte sequence as a known protocol family. */ -export function classifyProtocolFamily( - contentType: string | null, - path: string | null, -): ProtocolFamily | null { - if (contentType) { - if (contentType.includes("application/grpc")) return "grpc"; - if (contentType.includes("application/x-protobuf")) return "protobuf"; - if (contentType.includes("application/json-rpc")) return "json-rpc"; - if ( - contentType.includes("application/msgpack") || - contentType.includes("application/x-msgpack") - ) - return "messagepack"; - } - if (path) { - if (path.startsWith("/grpc.") || path.includes("/grpc/")) return "grpc"; - if (path.includes("/json-rpc") || path.includes("/jsonrpc")) - return "json-rpc"; - } - return null; -} - -/** Decode a JSON-RPC message from raw payload bytes. */ -export function decodeJsonRpc(rawPayload: Uint8Array): { - decoded_fields: DecodedField[]; - truncated: boolean; -} { - const text = new TextDecoder().decode(rawPayload); - const parsed = jsonValueSchema.parse(JSON.parse(text)); - const fields: DecodedField[] = []; - - if (typeof parsed === "object" && parsed !== null) { - for (const [key, value] of Object.entries(parsed)) { - fields.push({ - path: key, - wire_type: "json", - value, - inferred: false, - }); - } - } - - return { decoded_fields: fields, truncated: false }; -} - -/** Decode a MessagePack message (simplified: only checks basic structure). */ -export function decodeMessagePack(rawPayload: Uint8Array): { - decoded_fields: DecodedField[]; - truncated: boolean; -} { - const fields: DecodedField[] = []; - // MessagePack uses a single-byte type tag - if (rawPayload.length === 0) { - return { decoded_fields: [], truncated: false }; - } - const tag = rawPayload[0]; - if (tag === undefined) return { decoded_fields: [], truncated: false }; - // Map type: 0x80-0x8f (fixmap), 0xde (map16), 0xdf (map32) - if ((tag >= 0x80 && tag <= 0x8f) || tag === 0xde || tag === 0xdf) { - fields.push({ - path: "type", - wire_type: "msgpack", - value: "map", - inferred: false, - }); - } - // Array type: 0x90-0x9f (fixarray), 0xdc (array16), 0xdd (array32) - else if ((tag >= 0x90 && tag <= 0x9f) || tag === 0xdc || tag === 0xdd) { - fields.push({ - path: "type", - wire_type: "msgpack", - value: "array", - inferred: false, - }); - } else { - fields.push({ - path: "type", - wire_type: "msgpack", - value: "unknown", - inferred: true, - }); - } - return { - decoded_fields: fields, - truncated: false, - }; -} - -/** Decode a gRPC frame (5-byte header + protobuf payload). */ -export function decodeGrpcFrame(rawPayload: Uint8Array): { - decoded_fields: DecodedField[]; - truncated: boolean; -} { - // gRPC uses a 5-byte prefix: 1 byte compressed flag + 4 byte length - if (rawPayload.length < 5) { - return { - decoded_fields: [ - { - path: "error", - wire_type: "grpc", - value: "frame too short", - inferred: false, - }, - ], - truncated: false, - }; - } - const compressed = rawPayload[0] !== 0; - const length = - ((rawPayload[1] ?? 0) << 24) | - ((rawPayload[2] ?? 0) << 16) | - ((rawPayload[3] ?? 0) << 8) | - (rawPayload[4] ?? 0); - return { - decoded_fields: [ - { - path: "compressed", - wire_type: "grpc", - value: compressed, - inferred: false, - }, - { - path: "message_length", - wire_type: "grpc", - value: length, - inferred: false, - }, - ], - truncated: false, - }; -} - -/** Infer schema hypotheses from a set of decoded messages. */ -export function inferMessageSchema( - messages: readonly ProtocolMessage[], -): SchemaHypothesis[] { - const fieldCounts = new Map }>(); - for (const msg of messages) { - for (const field of msg.decoded_fields) { - const existing = fieldCounts.get(field.path); - if (existing) { - existing.count++; - if (field.wire_type) existing.types.add(field.wire_type); - } else { - fieldCounts.set(field.path, { - count: 1, - types: new Set(field.wire_type ?? "unknown"), - }); - } - } - } - const hypotheses: SchemaHypothesis[] = []; - for (const [field_name, data] of fieldCounts) { - hypotheses.push({ - field_name, - inferred_type: [...data.types].join("|"), - confidence: Math.min(1, data.count / messages.length || 0), - evidence_count: data.count, - }); - } - return hypotheses; -} diff --git a/src/domain/providerSelectionError.ts b/src/domain/providerSelectionError.ts index 2dbb182fa9..e6520848f8 100644 --- a/src/domain/providerSelectionError.ts +++ b/src/domain/providerSelectionError.ts @@ -1,5 +1,8 @@ import type { JsonValue } from "./jsonValue.js"; -import { AnalysisError } from "./analysisErrorBase.js"; +import { + AnalysisError, + type AnalysisCapturedOutput, +} from "./analysisErrorBase.js"; /** Stable failure classes for target-bound deep-provider selection. */ export type ProviderSelectionFailureReason = @@ -19,6 +22,7 @@ export interface ProviderSelectionRejection { /** Complete typed context for one failed deep-provider selection. */ export interface ProviderSelectionErrorOptions { + readonly capturedOutput?: AnalysisCapturedOutput; readonly operation?: string; readonly reason: ProviderSelectionFailureReason; readonly requestedProviderId: string; @@ -38,7 +42,12 @@ export class ProviderSelectionError extends AnalysisError { constructor(options: ProviderSelectionErrorOptions) { const operation = options.operation ?? "open_binary"; - super(providerSelectionDiagnostic(options)); + super( + providerSelectionDiagnostic(options), + options.capturedOutput === undefined + ? undefined + : { capturedOutput: options.capturedOutput }, + ); this.operation = operation; this.reason = options.reason; this.requestedProviderId = options.requestedProviderId; diff --git a/src/domain/reconstructionCoverage.test.ts b/src/domain/reconstructionCoverage.test.ts index 01b0d329ff..4c0b418318 100644 --- a/src/domain/reconstructionCoverage.test.ts +++ b/src/domain/reconstructionCoverage.test.ts @@ -7,6 +7,7 @@ import { } from "./reconstructionCoverage.fixture.js"; import { createReconstructionCoverageData, + createReconstructionVerifierContract, evaluateReconstructionClosure, } from "./reconstructionCoverage.js"; @@ -159,6 +160,168 @@ describe("reconstruction coverage closure", () => { }); describe("reconstruction coverage verifier observations", () => { + it("uses one contract and result for each claim covered by a shared verifier", () => { + const workspace = completeReconstructionCoverageData(); + const contract = workspace.verifier_contracts[0]; + const result = workspace.verifier_results[0]; + const claim = workspace.claims[0]; + const boundary = workspace.boundaries[0]; + if ( + contract === undefined || + result === undefined || + claim === undefined || + boundary === undefined + ) + throw new Error("Expected complete reconstruction coverage fixture"); + const secondClaimId = "claim.cli.version"; + const sharedContract = createReconstructionVerifierContract({ + verifier_id: contract.verifier_id, + claim_ids: [...contract.claim_ids, secondClaimId], + dimensions: contract.dimensions, + authority: contract.authority, + max_age_ms: contract.max_age_ms, + minimum_repeats: contract.minimum_repeats, + normalization_sha256: contract.normalization_sha256, + normalization_removes_dimensions: + contract.normalization_removes_dimensions, + }); + const sharedResult = { + ...result, + contract_sha256: sharedContract.contract_sha256, + covered_claim_ids: [...result.covered_claim_ids, secondClaimId], + }; + const shared = createReconstructionCoverageData({ + ...workspace, + claims: [ + ...workspace.claims, + { ...claim, claim_id: secondClaimId, title: "Version output matches" }, + ], + verifier_contracts: [sharedContract], + verifier_results: [sharedResult], + boundaries: [ + { + ...boundary, + required_claim_ids: [...boundary.required_claim_ids, secondClaimId], + }, + ], + }); + + expect( + evaluateReconstructionClosure( + shared, + boundary.boundary_id, + RECONSTRUCTION_COVERAGE_NOW, + ), + ).toMatchObject({ + status: "ready", + summary: { required_claims: 2, reasons: 0 }, + }); + }); +}); + +describe("reconstruction coverage verifier selection", () => { + it("keeps ambiguous verifier contracts and missing results actionable", () => { + const workspace = completeReconstructionCoverageData(); + const contract = workspace.verifier_contracts[0]; + const result = workspace.verifier_results[0]; + const boundary = workspace.boundaries[0]; + if ( + contract === undefined || + result === undefined || + boundary === undefined + ) + throw new Error("Expected complete reconstruction coverage fixture"); + const alternateContract = createReconstructionVerifierContract({ + verifier_id: "verify.cli.help.alternate", + claim_ids: contract.claim_ids, + dimensions: contract.dimensions, + authority: contract.authority, + max_age_ms: contract.max_age_ms, + minimum_repeats: contract.minimum_repeats, + normalization_sha256: contract.normalization_sha256, + normalization_removes_dimensions: + contract.normalization_removes_dimensions, + }); + const ambiguous = createReconstructionCoverageData({ + ...workspace, + verifier_contracts: [...workspace.verifier_contracts, alternateContract], + verifier_results: [ + ...workspace.verifier_results, + { + ...result, + verifier_id: alternateContract.verifier_id, + contract_sha256: alternateContract.contract_sha256, + }, + ], + }); + expect( + evaluateReconstructionClosure( + ambiguous, + boundary.boundary_id, + RECONSTRUCTION_COVERAGE_NOW, + ).reasons, + ).toEqual([ + expect.objectContaining({ + code: "verifier-duplicate", + subject_id: "claim.cli.help", + }), + ]); + + const missing = createReconstructionCoverageData({ + ...workspace, + verifier_results: [], + }); + expect( + evaluateReconstructionClosure( + missing, + boundary.boundary_id, + RECONSTRUCTION_COVERAGE_NOW, + ).reasons, + ).toEqual([ + expect.objectContaining({ + code: "verifier-result-missing", + subject_id: "claim.cli.help", + }), + ]); + }); +}); + +describe("reconstruction coverage verifier result ordering", () => { + it("preserves the first input result when verifier timestamps tie", () => { + const workspace = completeReconstructionCoverageData(); + const result = workspace.verifier_results[0]; + const boundary = workspace.boundaries[0]; + if (result === undefined || boundary === undefined) + throw new Error("Expected complete reconstruction coverage fixture"); + const failed = { ...result, status: "fail" as const }; + const firstPass = createReconstructionCoverageData({ + ...workspace, + verifier_results: [result, failed], + }); + const firstFail = createReconstructionCoverageData({ + ...workspace, + verifier_results: [failed, result], + }); + + expect( + evaluateReconstructionClosure( + firstPass, + boundary.boundary_id, + RECONSTRUCTION_COVERAGE_NOW, + ).status, + ).toBe("ready"); + expect( + evaluateReconstructionClosure( + firstFail, + boundary.boundary_id, + RECONSTRUCTION_COVERAGE_NOW, + ), + ).toMatchObject({ + status: "failed", + reasons: [expect.objectContaining({ code: "verifier-failed" })], + }); + }); + it("orders offset-bearing verifier timestamps chronologically", () => { const workspace = completeReconstructionCoverageData(); const result = workspace.verifier_results[0]; diff --git a/src/domain/reconstructionCoverageEvaluation.ts b/src/domain/reconstructionCoverageEvaluation.ts index ceb38c0cfa..ea82e56363 100644 --- a/src/domain/reconstructionCoverageEvaluation.ts +++ b/src/domain/reconstructionCoverageEvaluation.ts @@ -140,6 +140,7 @@ export const evaluateReconstructionClaims = ( context: ReconstructionEvaluationContext, ): void => { const claims = indexUnique(context.coverage.claims, "claim_id"); + const indexes = createVerifierIndexes(context.coverage); for (const claimId of context.boundary.required_claim_ids) { const claim = claims.get(claimId); if (claim === undefined) { @@ -151,9 +152,7 @@ export const evaluateReconstructionClaims = ( ); continue; } - const contracts = context.coverage.verifier_contracts.filter( - ({ claim_ids: ids }) => ids.includes(claimId), - ); + const contracts = indexes.contractsByClaim.get(claimId) ?? []; if (contracts.length === 0) { addReason( context, @@ -174,22 +173,94 @@ export const evaluateReconstructionClaims = ( } const contract = contracts[0]; if (contract !== undefined) - evaluateVerifierResult(context, claim, contract); + evaluateVerifierResult(context, claim, contract, indexes); } }; +interface VerifierIndexes { + readonly contractsByClaim: ReadonlyMap< + string, + readonly ReconstructionVerifierContract[] + >; + readonly latestResultsByVerifier: ReadonlyMap< + string, + ReconstructionCoverageData["verifier_results"][number] + >; + readonly artifactDigests: ReadonlySet; + readonly ownerDigests: ReadonlySet; + readonly surfaces: ReadonlyMap< + string, + ReconstructionCoverageData["surfaces"][number] + >; + readonly artifacts: ReadonlyMap< + string, + ReconstructionCoverageData["artifacts"][number] + >; + readonly ownersBySurface: ReadonlyMap< + string, + readonly ReconstructionCoverageData["owners"][number][] + >; +} + +const createVerifierIndexes = ( + coverage: ReconstructionCoverageData, +): VerifierIndexes => { + const contractsByClaim = new Map(); + for (const contract of coverage.verifier_contracts) + for (const claimId of new Set(contract.claim_ids)) { + const contracts = contractsByClaim.get(claimId) ?? []; + contracts.push(contract); + contractsByClaim.set(claimId, contracts); + } + + const latestResultsByVerifier = new Map< + string, + ReconstructionCoverageData["verifier_results"][number] + >(); + for (const result of coverage.verifier_results) { + const latest = latestResultsByVerifier.get(result.verifier_id); + // Stable descending sort used to choose the first input record on ties. + if ( + latest === undefined || + Date.parse(result.observed_at) > Date.parse(latest.observed_at) + ) + latestResultsByVerifier.set(result.verifier_id, result); + } + + const ownersBySurface = new Map< + string, + ReconstructionCoverageData["owners"][number][] + >(); + for (const owner of coverage.owners) { + const owners = ownersBySurface.get(owner.surface_id) ?? []; + owners.push(owner); + ownersBySurface.set(owner.surface_id, owners); + } + + return { + contractsByClaim, + latestResultsByVerifier, + artifactDigests: new Set( + coverage.artifacts.map(({ artifact_sha256: value }) => value), + ), + ownerDigests: new Set( + coverage.owners.flatMap(({ ownership }) => + ownership.disposition === "implemented" ? [ownership.owner_sha256] : [], + ), + ), + surfaces: indexUnique(coverage.surfaces, "surface_id"), + artifacts: indexUnique(coverage.artifacts, "artifact_id"), + ownersBySurface, + }; +}; + const evaluateVerifierResult = ( context: ReconstructionEvaluationContext, claim: ReconstructionCoverageData["claims"][number], contract: ReconstructionVerifierContract, + indexes: VerifierIndexes, ): void => { - const results = context.coverage.verifier_results - .filter(({ verifier_id: id }) => id === contract.verifier_id) - .sort( - (left, right) => - Date.parse(right.observed_at) - Date.parse(left.observed_at), - ); - const result = results[0]; + const result = indexes.latestResultsByVerifier.get(contract.verifier_id); if (result === undefined) { addReason( context, @@ -200,7 +271,7 @@ const evaluateVerifierResult = ( return; } addEvidence(context, result.evidence_ids); - if (!verifierResultIsCompatible(context, claim, contract, result)) { + if (!verifierResultIsCompatible(claim, contract, result, indexes)) { addReason( context, "verifier-result-incompatible", @@ -239,40 +310,23 @@ const evaluateVerifierResult = ( }; const verifierResultIsCompatible = ( - context: ReconstructionEvaluationContext, claim: ReconstructionCoverageData["claims"][number], contract: ReconstructionVerifierContract, result: ReconstructionCoverageData["verifier_results"][number], + indexes: VerifierIndexes, ): boolean => { - const artifactDigests = new Set( - context.coverage.artifacts.map(({ artifact_sha256: value }) => value), - ); - const ownerDigests = new Set( - context.coverage.owners.flatMap(({ ownership }) => - ownership.disposition === "implemented" ? [ownership.owner_sha256] : [], - ), - ); - const surfaces = new Map( - context.coverage.surfaces.map((surface) => [surface.surface_id, surface]), - ); - const artifacts = new Map( - context.coverage.artifacts.map((artifact) => [ - artifact.artifact_id, - artifact, - ]), - ); const expectedArtifactDigests = claim.surface_ids.flatMap((surfaceId) => { - const surface = surfaces.get(surfaceId); + const surface = indexes.surfaces.get(surfaceId); const artifact = - surface === undefined ? undefined : artifacts.get(surface.artifact_id); + surface === undefined + ? undefined + : indexes.artifacts.get(surface.artifact_id); return artifact === undefined ? [] : [artifact.artifact_sha256]; }); - const expectedOwnerDigests = context.coverage.owners.flatMap( - ({ surface_id: surfaceId, ownership }) => - claim.surface_ids.includes(surfaceId) && - ownership.disposition === "implemented" - ? [ownership.owner_sha256] - : [], + const expectedOwnerDigests = claim.surface_ids.flatMap((surfaceId) => + (indexes.ownersBySurface.get(surfaceId) ?? []).flatMap(({ ownership }) => + ownership.disposition === "implemented" ? [ownership.owner_sha256] : [], + ), ); return ( result.contract_sha256 === contract.contract_sha256 && @@ -290,11 +344,13 @@ const verifierResultIsCompatible = ( result.covered_dimensions.every((item) => contract.dimensions.includes(item), ) && - result.artifact_sha256s.every((item) => artifactDigests.has(item)) && + result.artifact_sha256s.every((item) => + indexes.artifactDigests.has(item), + ) && expectedArtifactDigests.every((item) => result.artifact_sha256s.includes(item), ) && - result.owner_sha256s.every((item) => ownerDigests.has(item)) && + result.owner_sha256s.every((item) => indexes.ownerDigests.has(item)) && expectedOwnerDigests.every((item) => result.owner_sha256s.includes(item)) && result.repeats >= contract.minimum_repeats && claim.required_authority === contract.authority diff --git a/src/domain/reconstructionVerification.test.ts b/src/domain/reconstructionVerification.test.ts index a2a8573d62..2de3e77178 100644 --- a/src/domain/reconstructionVerification.test.ts +++ b/src/domain/reconstructionVerification.test.ts @@ -7,7 +7,7 @@ import { verifyReconstruction, } from "./reconstructionVerification.js"; import type { JsonValue } from "./jsonValue.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./process/processCapture.fixture.js"; import { reconstructionSpecificationSchema } from "./reconstructionVerificationSchemas.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js"; import { createResidualUnknown } from "./residualUnknown.js"; @@ -23,6 +23,7 @@ const source = ( digit: string, authority: "controlled-replay" | "shipped-artifact", confidence: "observed" | "derived" = "observed", + captureResult: JsonValue = EMPTY_PROCESS_CAPTURE_EXAMPLE, ): Evidence => createEvidence( { @@ -41,16 +42,26 @@ const source = ( ? "capture_process_scenario" : "inventory_artifact", parameters: {}, - result: - authority === "controlled-replay" - ? EMPTY_PROCESS_CAPTURE_EXAMPLE - : { digit }, + result: authority === "controlled-replay" ? captureResult : { digit }, confidence, authority, environment: authority === "controlled-replay" ? environment : null, }, ); +const legacyV3Capture = () => { + const { + selected_executable_sha256: _selectedDigest, + executable_identity: _executableIdentity, + executable_sha256, + ...legacyManifest + } = EMPTY_PROCESS_CAPTURE_EXAMPLE.manifest; + return { + ...EMPTY_PROCESS_CAPTURE_EXAMPLE, + manifest: { ...legacyManifest, executable_sha256 }, + }; +}; + const processResult = ( terminal: "unchanged" | "changed" | "unknown" = "unchanged", ): JsonValue => ({ @@ -275,7 +286,48 @@ describe("reconstruction verification", () => { ]), ); }); +}); +describe("saved v3 reconstruction capture compatibility", () => { + it("verifies saved v3 process captures and rejects a malformed legacy digest", () => { + const legacy = legacyV3Capture(); + const left = source("1", "controlled-replay", "observed", legacy); + const right = source("2", "controlled-replay"); + const comparison = processComparison(left, right, processResult()); + const bundle = createEvidenceBundle([comparison, left, right]); + + expect( + verifyReconstruction(behavioralSpec(comparison), bundle), + ).toMatchObject({ + status: "pass", + summary: { total: 1, passed: 1, failed: 0, unknown: 0 }, + }); + + const malformedLegacy = { + ...legacy, + manifest: { ...legacy.manifest, executable_sha256: null }, + }; + const malformedLeft = source( + "3", + "controlled-replay", + "observed", + malformedLegacy, + ); + const malformedComparison = processComparison( + malformedLeft, + right, + processResult(), + ); + expect(() => + verifyReconstruction( + behavioralSpec(malformedComparison), + createEvidenceBundle([malformedComparison, malformedLeft, right]), + ), + ).toThrow(); + }); +}); + +describe("reconstruction verification outcomes", () => { it("fails an authoritative observed disagreement", () => { const left = source("1", "controlled-replay"); const right = source("2", "controlled-replay"); diff --git a/src/domain/reconstructionVerification.ts b/src/domain/reconstructionVerification.ts index 1900c93bea..3de1d85096 100644 --- a/src/domain/reconstructionVerification.ts +++ b/src/domain/reconstructionVerification.ts @@ -12,14 +12,14 @@ import { functionComparisonResultSchema } from "./functionComparison.js"; import { PROCESS_PROVIDER, isProcessEvidenceProvider, -} from "./processEvidenceProvider.js"; +} from "./process/processEvidenceProvider.js"; import { functionDossierSchema } from "./hopperValues.js"; import { deriveProcessComparisonStatus, PROCESS_COMPARISON_DIMENSIONS, processCaptureComparisonSchema, - processCaptureSchema, -} from "./processCapture.js"; + parseProcessCapture, +} from "./process/processCapture.js"; import { reconstructionClaimResultSchema, reconstructionSpecificationSchema, @@ -199,7 +199,7 @@ const validateSourceKinds = ( ); for (const source of sources) { if (claim.kind === "behavioral") - processCaptureSchema.parse(source.normalized_result); + parseProcessCapture(source.normalized_result); else if (claim.kind === "structural-function") functionDossierSchema.parse(source.normalized_result); else artifactInventoryResultSchema.parse(source.normalized_result); diff --git a/src/domain/reconstructionVerificationSchemas.ts b/src/domain/reconstructionVerificationSchemas.ts index 023cd3b11c..4ce88ba7a0 100644 --- a/src/domain/reconstructionVerificationSchemas.ts +++ b/src/domain/reconstructionVerificationSchemas.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { digestSchema } from "./../domain/digests.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; -import { PROCESS_COMPARISON_DIMENSIONS } from "./processComparison.js"; +import { PROCESS_COMPARISON_DIMENSIONS } from "./process/processComparison.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const unknownIdSchema = prefixedDigestSchema("unk"); diff --git a/src/domain/residualUnknown.ts b/src/domain/residualUnknown.ts index 396f72dcb8..7695c340c2 100644 --- a/src/domain/residualUnknown.ts +++ b/src/domain/residualUnknown.ts @@ -167,12 +167,13 @@ export const recordUnknownInputSchema = residualUnknownObjectSchema .extend({ supporting_evidence_ids: z.array(evidenceIdSchema).default([]), contradicting_evidence_ids: z.array(evidenceIdSchema).default([]), - }); + }) + .strict(); export type RecordUnknownInput = z.infer; /** Explicit optimistic-concurrency update command for one unknown. */ -export const updateUnknownInputSchema = z.object({ +export const updateUnknownInputSchema = z.strictObject({ unknown_id: unknownIdSchema, expected_revision: z.number().int().min(1), status: residualUnknownObjectSchema.shape.status, diff --git a/src/domain/runtimeIdentification.ts b/src/domain/runtimeIdentification.ts deleted file mode 100644 index 8a58a096d7..0000000000 --- a/src/domain/runtimeIdentification.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; -import { z } from "zod"; - -import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js"; -import { evidenceSchema } from "./evidence.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; - -const evidenceIdSchema = prefixedDigestSchema("ev"); -const runtimeFamilySchema = z.enum([ - "android", - "apple", - "javascript", - "jvm", - "native", - "webassembly", -]); -const observationSchema = z.strictObject({ - path: z.string().min(1), - artifact_id: prefixedDigestSchema("art"), - sha256: digestSchema, - format: z.string().min(1), -}); -const runtimeObservationShape = { - family: runtimeFamilySchema, - observations: z.array(observationSchema), -}; -const identifiedRuntimeSchema = z.discriminatedUnion("inspection", [ - z.strictObject({ - ...runtimeObservationShape, - inspection: z.literal("available"), - provider_id: z.string().min(1), - reason: z.null(), - }), - z.strictObject({ - ...runtimeObservationShape, - inspection: z.literal("provider-missing"), - provider_id: z.null(), - reason: z.string().min(1), - }), - z.strictObject({ - ...runtimeObservationShape, - inspection: z.literal("provider-selection-required"), - provider_id: z.null(), - reason: z.string().min(1), - }), -]); - -/** Authenticated artifact inventory pages to classify by runtime family. */ -const runtimeIdentificationInputSchema = z.strictObject({ - inventory_evidence: z.array(evidenceSchema).min(1), -}); - -/** Provider-neutral runtime identification with explicit tooling availability. */ -export const runtimeIdentificationResultSchema = z.strictObject({ - identification_id: prefixedDigestSchema("rid"), - root_sha256: digestSchema, - root_format: z.string().min(1), - source_evidence_ids: z.array(evidenceIdSchema).min(1), - runtimes: z.array(identifiedRuntimeSchema), - coverage: z.strictObject({ - status: z.enum(["complete-within-inventory", "partial"]), - inventory_complete: z.boolean(), - }), - limitations: z.array(z.string().min(1)), -}); - -export type RuntimeIdentificationInput = z.input< - typeof runtimeIdentificationInputSchema ->; -export type RuntimeIdentificationResult = z.infer< - typeof runtimeIdentificationResultSchema ->; -type RuntimeFamily = z.infer; -type Observation = z.infer; -type RuntimeProvider = - | { - readonly inspection: "available"; - readonly id: string; - readonly reason: null; - } - | { - readonly inspection: "provider-missing" | "provider-selection-required"; - readonly id: null; - readonly reason: string; - }; - -const PROVIDERS: Readonly> = { - android: { - inspection: "available", - id: "rea-android-application", - reason: null, - }, - apple: { inspection: "available", id: "rea-apple-application", reason: null }, - javascript: { - inspection: "available", - id: "rea-javascript-application", - reason: null, - }, - jvm: { - inspection: "provider-missing", - id: null, - reason: "Install or configure a JVM bytecode-analysis provider.", - }, - native: { - inspection: "provider-selection-required", - id: null, - reason: - "Select an available native deep-analysis provider for this target and host.", - }, - webassembly: { - inspection: "provider-missing", - id: null, - reason: "Install or configure a WebAssembly semantic-analysis provider.", - }, -}; - -/** Identify runtime families without executing or semantically decoding target bytes. */ -export const identifyRuntimes = ( - input: RuntimeIdentificationInput, -): RuntimeIdentificationResult => { - const parsed = runtimeIdentificationInputSchema.parse(input); - const { evidence, inventory } = parseArtifactInventoryEvidence( - parsed.inventory_evidence, - ); - const nodes = new Map( - inventory.nodes.map((node) => [node.artifact_id, node]), - ); - const grouped = new Map(); - const add = (family: RuntimeFamily, observation: Observation): void => { - const observations = grouped.get(family) ?? []; - observations.push(observation); - grouped.set(family, observations); - }; - for (const occurrence of inventory.occurrences) { - if (occurrence.artifact_id === null) continue; - const node = nodes.get(occurrence.artifact_id); - if (node === undefined) - throw new TypeError("Runtime observation has no artifact node"); - const observation = { - path: occurrence.logical_path, - artifact_id: node.artifact_id, - sha256: node.sha256, - format: node.format, - }; - for (const family of familiesFor(node.format, occurrence.logical_path)) - add(family, observation); - } - const rootObservation = inventory.nodes.find( - ({ sha256 }) => sha256 === inventory.manifest.root_sha256, - ); - if (rootObservation !== undefined) - for (const family of familiesFor(inventory.manifest.root_format, ".")) - add(family, { - path: ".", - artifact_id: rootObservation.artifact_id, - sha256: rootObservation.sha256, - format: rootObservation.format, - }); - const runtimes = [...grouped.entries()] - .sort(([left], [right]) => compare(left, right)) - .map(([family, values]) => { - const unique = deduplicate(values); - const provider = providerFor(family, inventory.manifest.root_format); - return { - family, - ...runtimeInspection(provider), - observations: unique, - }; - }); - const limitations = [ - ...(!inventory.complete - ? [ - "Source inventory pages are incomplete; absent runtimes remain unknown.", - ] - : []), - "Runtime families are identified from exact artifact formats and paths; semantic claims require the named provider.", - ]; - const withoutId = { - root_sha256: inventory.manifest.root_sha256, - root_format: inventory.manifest.root_format, - source_evidence_ids: evidence - .map(({ evidence_id: id }) => id) - .sort(compare), - runtimes, - coverage: { - status: inventory.complete - ? ("complete-within-inventory" as const) - : ("partial" as const), - inventory_complete: inventory.complete, - }, - limitations, - }; - return runtimeIdentificationResultSchema.parse({ - ...withoutId, - identification_id: `rid_${digest(withoutId)}`, - }); -}; - -const providerFor = ( - family: RuntimeFamily, - rootFormat: string, -): RuntimeProvider => - family === "android" && rootFormat !== "apk" - ? { - inspection: "provider-selection-required", - id: null, - reason: - "Extract or select an APK root before using the Android application provider.", - } - : PROVIDERS[family]; - -const runtimeInspection = (provider: RuntimeProvider) => - provider.inspection === "available" - ? { - inspection: provider.inspection, - provider_id: provider.id, - reason: null, - } - : { - inspection: provider.inspection, - provider_id: null, - reason: provider.reason, - }; - -const familiesFor = (format: string, path: string): RuntimeFamily[] => { - const families = new Set(); - if ( - format === "apk" || - format === "dex" || - /(?:^|\/)[^/]+\.dex$/iu.test(path) || - /(?:^|\/)AndroidManifest\.xml$/u.test(path) - ) - families.add("android"); - if (format === "ipa") families.add("apple"); - if (format === "javascript-bundle") families.add("javascript"); - if (format === "jvm-class" || /(?:^|\/)[^/]+\.class$/iu.test(path)) - families.add("jvm"); - if ( - ["elf", "mach-o", "mach-o-universal", "pe", "dos-mz", "dos-com"].includes( - format, - ) - ) - families.add("native"); - if (format === "webassembly" || /(?:^|\/)[^/]+\.wasm$/iu.test(path)) - families.add("webassembly"); - return [...families]; -}; - -const deduplicate = (values: readonly Observation[]): Observation[] => - [ - ...new Map( - values.map((value) => [`${value.path}\0${value.artifact_id}`, value]), - ).values(), - ].sort((left, right) => compare(left.path, right.path)); -const compare = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; -const digest = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Runtime identification is not canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); -}; diff --git a/src/domain/staticRuntimeCorrelation.ts b/src/domain/staticRuntimeCorrelation.ts index 69d36b8553..d5558ad277 100644 --- a/src/domain/staticRuntimeCorrelation.ts +++ b/src/domain/staticRuntimeCorrelation.ts @@ -10,13 +10,13 @@ import { functionComparisonResultSchema } from "./functionComparison.js"; import { PROCESS_PROVIDER, isProcessEvidenceProvider, -} from "./processEvidenceProvider.js"; +} from "./process/processEvidenceProvider.js"; import type { JsonValue } from "./jsonValue.js"; import { comparisonStatusSchema, processCaptureComparisonSchema, -} from "./processCapture.js"; -import { PROCESS_COMPARISON_DIMENSIONS } from "./processComparison.js"; +} from "./process/processCapture.js"; +import { PROCESS_COMPARISON_DIMENSIONS } from "./process/processComparison.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); diff --git a/src/domain/unicodeCodePointOrder.ts b/src/domain/unicodeCodePointOrder.ts index 42acf2b068..163039364b 100644 --- a/src/domain/unicodeCodePointOrder.ts +++ b/src/domain/unicodeCodePointOrder.ts @@ -3,12 +3,16 @@ export const compareUnicodeCodePoints = ( left: string, right: string, ): number => { - const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0); - const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0); - const length = Math.min(leftPoints.length, rightPoints.length); - for (let index = 0; index < length; index += 1) { - const difference = (leftPoints[index] ?? 0) - (rightPoints[index] ?? 0); + let leftIndex = 0; + let rightIndex = 0; + while (leftIndex < left.length && rightIndex < right.length) { + const leftPoint = left.codePointAt(leftIndex) ?? 0; + const rightPoint = right.codePointAt(rightIndex) ?? 0; + const difference = leftPoint - rightPoint; if (difference !== 0) return difference; + leftIndex += leftPoint > 0xffff ? 2 : 1; + rightIndex += rightPoint > 0xffff ? 2 : 1; } - return leftPoints.length - rightPoints.length; + if (leftIndex === left.length && rightIndex === right.length) return 0; + return leftIndex === left.length ? -1 : 1; }; diff --git a/src/domain/webCaptureDiff.ts b/src/domain/webCaptureDiff.ts index ea8aa0420a..d92c2848b6 100644 --- a/src/domain/webCaptureDiff.ts +++ b/src/domain/webCaptureDiff.ts @@ -249,6 +249,13 @@ const networkMap = ( status: request.status, mime_type: request.mime_type, encoded_data_length: request.encoded_data_length, + redirects: (request.redirects ?? []).map( + ({ + request_timestamp: _requestTimestamp, + redirect_event_timestamp: _redirectEventTimestamp, + ...redirect + }) => redirect, + ), initiator: request.initiator, body_shapes: request.body_shapes, }); diff --git a/src/domain/webNetworkCapture.ts b/src/domain/webNetworkCapture.ts new file mode 100644 index 0000000000..bda9334bb1 --- /dev/null +++ b/src/domain/webNetworkCapture.ts @@ -0,0 +1,141 @@ +import { z } from "zod"; +import { jsonValueSchema } from "./jsonValue.js"; + +/** Complete historical evidence budgets; exceeding a budget returns no partial capture. */ +export const WEB_NETWORK_CAPTURE_LIMITS = { + inputBytes: 32 * 1024 * 1024, + outputBytes: 96 * 1024 * 1024, + timeoutMs: 30_000, + depth: 64, +} as const; + +/** Inspect an explicit retained capture without contacting its recorded destinations. */ +export const inspectWebNetworkCaptureInputSchema = z.strictObject({ + capture_path: z + .string() + .min(1) + .describe("Absolute local HAR or mitmproxy capture path."), + format: z.enum(["har", "mitmproxy"]), + record_ordinals: z + .array(z.number().int().nonnegative()) + .min(1) + .optional() + .describe( + "Zero-based producer record ordinals, in caller-selected order. Omit to inspect every record.", + ), + sensitive_values: z + .array(z.string().min(1)) + .default([]) + .describe( + "Literal text excluded from capture payloads, byte/numeric projections, producer coordinates, artifact paths/digests and diagnostic text. Declaration strings are never persisted in Evidence parameters. Canonical contract keys, enums and provider identities remain intact.", + ), +}); + +const pointerSchema = z + .string() + .describe("RFC 6901 pointer in the producer's reported object."); +const redactionSchema = z.union([ + z.strictObject({ + pointer: pointerSchema, + reason: z.enum(["transport-credential", "explicit-sensitive-value"]), + }), + z.strictObject({ + pointer: pointerSchema, + reason: z.literal("explicit-sensitive-value"), + scope: z + .literal("property-name") + .describe( + "An omitted property name; pointer identifies its actual parent, not an invented child coordinate.", + ), + }), +]); +const binaryBase = { + pointer: pointerSchema, + representation: z.enum(["producer-bytes", "har-base64-content"]), +}; +const binarySchema = z.discriminatedUnion("state", [ + z.strictObject({ + ...binaryBase, + state: z.literal("retained"), + content_base64: z.string(), + bytes: z.number().int().nonnegative(), + sha256: z.string().regex(/^[0-9a-f]{64}$/), + }), + z.strictObject({ + ...binaryBase, + state: z.literal("redacted"), + content_base64: z.null(), + bytes: z.null(), + sha256: z.null(), + }), +]); +const numberSchema = z.strictObject({ + pointer: pointerSchema, + producer_type: z.enum(["json-number", "integer", "float"]), + literal: z.string(), +}); + +/** Preserve producer representations with independent sidecars rather than invented browser IDs. */ +export const webNetworkCaptureRecordSchema = z.strictObject({ + ordinal: z.number().int().nonnegative(), + location: z.discriminatedUnion("kind", [ + z.strictObject({ + kind: z.literal("unknown"), + reason: z.literal("explicit-sensitive-value"), + }), + z.strictObject({ kind: z.literal("json-pointer"), pointer: pointerSchema }), + z.strictObject({ + kind: z.literal("byte-range"), + offset: z.number().int().nonnegative(), + bytes: z.number().int().positive(), + }), + ]), + reported: jsonValueSchema, + binary_fields: z.array(binarySchema), + numeric_literals: z.array(numberSchema), + redactions: z.array(redactionSchema), + limitations: z.array(z.string()), +}); + +/** Historical capture inspection has file identity, producer provenance and explicit unknowns. */ +export const webNetworkCaptureSchema = z.strictObject({ + artifact: z.strictObject({ + path: z + .string() + .describe( + "Selected artifact path; empty when explicitly excluded from Evidence.", + ), + sha256: z + .string() + .regex(/^[0-9a-f]{64}$/) + .nullable() + .describe( + "Observed artifact SHA-256; null only when explicitly excluded by a sensitive declaration.", + ), + bytes: z.number().int().nonnegative(), + }), + format: z.enum(["har", "mitmproxy"]), + decoder: z.strictObject({ + id: z.string().min(1), + name: z.string().min(1), + version: z.string().min(1), + }), + container: z.strictObject({ + reported: jsonValueSchema, + numeric_literals: z.array(numberSchema), + redactions: z.array(redactionSchema), + records_pointer: pointerSchema.nullable(), + }), + total_records: z.number().int().nonnegative(), + records: z.array(webNetworkCaptureRecordSchema), + runtime_attribution: z.literal("unknown"), + limitations: z.array(z.string()), +}); + +export type InspectWebNetworkCaptureInput = z.output< + typeof inspectWebNetworkCaptureInputSchema +>; +export type WebNetworkCapture = z.output; +export type WebNetworkCaptureRecord = z.output< + typeof webNetworkCaptureRecordSchema +>; diff --git a/src/domain/webNetworkCaptureCoordinates.ts b/src/domain/webNetworkCaptureCoordinates.ts new file mode 100644 index 0000000000..6cba46cd93 --- /dev/null +++ b/src/domain/webNetworkCaptureCoordinates.ts @@ -0,0 +1,68 @@ +import { redactExplicitText } from "./explicitSensitiveValues.js"; +import type { WebNetworkCapture } from "./webNetworkCapture.js"; + +/** Exclude marked source coordinates without inventing a substitute record identity. */ +export const excludeCaptureCoordinates = ( + value: WebNetworkCapture, + sensitiveValues: readonly string[], +): WebNetworkCapture => { + if (sensitiveValues.length === 0) return value; + let excluded = false; + const sensitive = (pointer: string): boolean => + sensitiveValues.some((literal) => pointer.includes(literal)); + const sidecars = ( + items: readonly T[], + ): T[] => + items.filter((item) => { + if (!sensitive(item.pointer)) return true; + excluded = true; + return false; + }); + const records = value.records.map((record) => { + const location = + (record.location.kind === "json-pointer" && + sensitive(record.location.pointer)) || + (record.location.kind === "byte-range" && + (sensitive(String(record.location.offset)) || + sensitive(String(record.location.bytes)))) + ? { + kind: "unknown" as const, + reason: "explicit-sensitive-value" as const, + } + : record.location; + if (location !== record.location) excluded = true; + return { + ...record, + location, + binary_fields: sidecars(record.binary_fields), + numeric_literals: sidecars(record.numeric_literals), + redactions: sidecars(record.redactions), + }; + }); + const recordsPointer = value.container.records_pointer; + const records_pointer = + recordsPointer !== null && sensitive(recordsPointer) + ? null + : recordsPointer; + if (records_pointer !== recordsPointer) excluded = true; + const container = { + ...value.container, + records_pointer, + numeric_literals: sidecars(value.container.numeric_literals), + redactions: sidecars(value.container.redactions), + }; + return { + ...value, + container, + records, + limitations: excluded + ? [ + ...value.limitations, + redactExplicitText( + "Marked source coordinates and their sidecars are excluded; affected record locations remain unknown.", + sensitiveValues, + ), + ] + : value.limitations, + }; +}; diff --git a/src/dotnet/ManagedArtifactFailures.test.ts b/src/dotnet/ManagedArtifactFailures.test.ts index b80e144632..1647157b5e 100644 --- a/src/dotnet/ManagedArtifactFailures.test.ts +++ b/src/dotnet/ManagedArtifactFailures.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js"; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js"; import { buildManagedPeFixture, buildNativePeFixture, @@ -43,4 +45,37 @@ describe("managed artifact failure classification", () => { expect.objectContaining({ code: "invalid-tables" }), ]); }); + + it("keeps malformed reference and attribute issues across inventory callers", () => { + const bytes = buildManagedPeFixture({ + references: [ + "System.Runtime", + "System.Runtime", + "UnityEngine.CoreModule", + ], + malformedAssemblyReferenceRows: [3], + malformedCustomAttributeRows: [1], + }); + const target = managedPeFixtureTarget(bytes); + const artifact = inspectManagedArtifactBytes(bytes, target); + const members = inspectManagedMembersBytes(bytes, target); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, target); + const issueCodes = ["invalid-heap-index", "invalid-row"]; + + expect(artifact.references.map(({ name }) => name)).toEqual([ + "System.Runtime", + "System.Runtime", + ]); + expect(artifact.coverage.issues.map(({ code }) => code)).toEqual( + issueCodes, + ); + expect(members.coverage.issues.map(({ code }) => code)).toEqual(issueCodes); + expect(boundaries.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ code: "invalid-heap-index" }), + expect.objectContaining({ code: "invalid-row" }), + ], + }); + }); }); diff --git a/src/dotnet/ManagedArtifactInspector.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts index e98e7aeafc..638dfd353e 100644 --- a/src/dotnet/ManagedArtifactInspector.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -9,8 +9,66 @@ import { buildManagedPeFixture, managedPeFixtureTarget, } from "./ManagedPe.fixture.js"; +import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js"; +import { readManagedPeLayout } from "./ManagedPeReader.js"; + +const metadataLayoutFor = (bytes: Buffer) => { + const pe = readManagedPeLayout(bytes); + if (pe.cli === null) throw new Error("fixture omitted CLI metadata"); + return readManagedMetadataLayout( + bytes, + pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "cli.metadata"), + pe.cli.metadata.size, + ); +}; describe("managed artifact inventory", () => { + it.each([ + { moduleRowCount: 0, assemblyRowCount: 1, scope: "metadata.Module" }, + { moduleRowCount: 2, assemblyRowCount: 1, scope: "metadata.Module" }, + { moduleRowCount: 1, assemblyRowCount: 2, scope: "metadata.Assembly" }, + ])("marks invalid identity table cardinality partial: %j", (options) => { + const bytes = buildManagedPeFixture(options); + const target = managedPeFixtureTarget(bytes); + const results = [ + inspectManagedArtifactBytes(bytes, target), + inspectManagedMembersBytes(bytes, target), + inspectManagedNativeBoundariesBytes(bytes, target), + ]; + + for (const result of results) { + expect(result.metadata.status).toBe("partial"); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + scope: options.scope, + }), + ], + }); + } + if (options.moduleRowCount === 0) expect(results[0]?.module).toBeNull(); + else expect(results[0]?.module?.name).toBe("Fixture.dll"); + }); + + it.each([ + { moduleRowCount: 1, assemblyRowCount: 0, targetFramework: null }, + { moduleRowCount: 1, assemblyRowCount: 1 }, + ])("keeps valid identity table cardinalities complete: %j", (options) => { + const bytes = buildManagedPeFixture(options); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.metadata.status).toBe("complete"); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); + expect(result.assembly?.name ?? null).toBe( + options.assemblyRowCount === 0 ? null : "Fixture.Managed", + ); + }); + it("statically inspects PE files with more than 96 sections", () => { const fixture = buildManagedPeFixture(); const peOffset = fixture.readUInt32LE(0x3c); @@ -77,7 +135,133 @@ describe("managed artifact inventory", () => { ]); expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); }); +}); +describe("managed artifact coded indexes", () => { + it("reports an invalid ManifestResource implementation while retaining the resource", () => { + const bytes = buildManagedPeFixture({ resourceImplementationRaw: 9 }); + const target = managedPeFixtureTarget(bytes); + const result = inspectManagedArtifactBytes(bytes, target); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, target); + + expect(result.resources).toMatchObject([ + { + name: "Fixture.resources", + implementation_token: null, + embedded: false, + }, + ]); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + scope: "metadata.ManifestResource:0x28000001", + detail: expect.stringContaining( + "coded index 0x9 is invalid: coded index selects row 2 in table 35, which has 1 rows", + ), + }), + ], + }); + const layout = metadataLayoutFor(bytes); + expect(result.coverage.issues[0]?.offset).toBe( + (result.resources[0]?.row_offset ?? 0) + 8 + layout.stringIndexSize, + ); + expect(boundaries.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + scope: "metadata.ManifestResource:0x28000001", + }), + ], + }); + }); + + it("reports an invalid CustomAttribute constructor coded index", () => { + const bytes = buildManagedPeFixture({ customAttributeTypeRaw: 19 }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes).toEqual([]); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + scope: "metadata.CustomAttribute:0x0c000001", + detail: expect.stringContaining( + "coded index 0x13 is invalid: coded index selects row 2 in table 10, which has 1 rows", + ), + }), + ], + }); + }); + + it("reports null for the required CustomAttribute constructor coded index", () => { + const bytes = buildManagedPeFixture({ customAttributeTypeRaw: 0 }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes).toEqual([]); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + detail: + "CustomAttribute constructor coded index 0x0 is null, but Type must reference a MethodDef or MemberRef row", + }), + ], + }); + }); + + it("reports coded-index column offsets after widened string and coded-index columns", () => { + const references = Array.from( + { length: 2_500 }, + (_, index) => `Reference.${"r".repeat(24)}.${String(index)}`, + ); + const bytes = buildManagedPeFixture({ + references, + extendsRaw: 9, + customAttributeTypeRaw: 19, + }); + const layout = metadataLayoutFor(bytes); + const artifact = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + const members = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + const extendsIssue = members.coverage.issues.find((issue) => + issue.scope.startsWith("metadata.TypeDef:"), + ); + const attributeIssue = artifact.coverage.issues.find((issue) => + issue.scope.startsWith("metadata.CustomAttribute:"), + ); + const typeDef = members.types[0]; + const attributeTable = layout.table(12); + + expect(layout.stringIndexSize).toBe(4); + expect(layout.codedIndexSize("HasCustomAttribute")).toBe(4); + expect(typeDef).toBeDefined(); + expect(extendsIssue?.offset).toBe( + (typeDef?.row_offset ?? 0) + 4 + 2 * layout.stringIndexSize, + ); + expect(attributeTable).toBeDefined(); + expect(attributeIssue?.offset).toBe( + (attributeTable?.offset ?? 0) + + layout.codedIndexSize("HasCustomAttribute"), + ); + }); +}); + +describe("managed artifact reference inventory", () => { it("returns all references inline while classifying from the full inventory", () => { const bytes = buildManagedPeFixture({ references: ["System.Runtime", "UnityEngine.CoreModule"], @@ -93,6 +277,60 @@ describe("managed artifact inventory", () => { expect(result.coverage.issues).toEqual([]); }); + it("preserves duplicate AssemblyRef rows in metadata order", () => { + const bytes = buildManagedPeFixture({ + references: [ + "UnityEngine.CoreModule", + "UnityEngine.CoreModule", + "System.Runtime", + ], + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.references.map(({ name }) => name)).toEqual([ + "UnityEngine.CoreModule", + "UnityEngine.CoreModule", + "System.Runtime", + ]); + expect(result.classification.runtime_family).toBe("unity-mono"); + }); +}); + +describe("managed fixture data layout", () => { + it("keeps ReadyToRun metadata and resources intact with large metadata", () => { + const resource = Buffer.alloc(2_048, 0x52); + const fieldSignature = Buffer.concat([ + Buffer.from([0x06]), + Buffer.alloc(8_000, 0x0f), + Buffer.from([0x08]), + ]); + const bytes = buildManagedPeFixture({ + fieldSignature, + readyToRun: true, + resourceData: resource, + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.classification).toMatchObject({ + status: "managed", + implementation: "cil-and-ready-to-run", + }); + expect(result.resources[0]).toMatchObject({ + embedded: true, + data_length: resource.length, + data_sha256: createHash("sha256").update(resource).digest("hex"), + }); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); + }); +}); + +describe("managed artifact metadata identity", () => { it("accepts CLI metadata GUIDs without RFC UUID version or variant bits", () => { const bytes = buildManagedPeFixture({ mvid: Buffer.from("3aebc60edc4a544b1f458b4ed40b33b1", "hex"), @@ -140,6 +378,55 @@ describe("managed artifact inventory", () => { }); }); +it("derives the string heap width after expanded AssemblyRef names", () => { + const references = Array.from( + { length: 5_000 }, + (_, index) => `Reference.${String(index).padStart(4, "0")}`, + ); + const bytes = buildManagedPeFixture({ references }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.references).toHaveLength(references.length); + expect(result.references.at(-1)?.name).toBe("Reference.4999"); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); + + const malformedBytes = buildManagedPeFixture({ + references, + malformedAssemblyReferenceRows: [5_000], + }); + const malformedResult = inspectManagedArtifactBytes( + malformedBytes, + managedPeFixtureTarget(malformedBytes), + ); + expect(malformedResult.references).toHaveLength(4_999); + expect(malformedResult.coverage).toMatchObject({ + state: "partial", + issues: [expect.objectContaining({ code: "invalid-heap-index" })], + }); +}); + +it("derives ResolutionScope and Implementation widths at 16,384 AssemblyRefs", () => { + const references = Array.from( + { length: 16_384 }, + (_, index) => `Reference.${String(index).padStart(5, "0")}`, + ); + const bytes = buildManagedPeFixture({ references }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.references).toHaveLength(references.length); + expect(result.references.at(-1)?.name).toBe("Reference.16383"); + expect(result.resources).toMatchObject([ + { name: "Fixture.resources", embedded: true, data_length: 13 }, + ]); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); +}); + it("preserves U+FEFF in references and fixed attribute strings", () => { const bytes = buildManagedPeFixture({ references: ["\uFEFFSystem.Runtime", "System.\uFEFFRuntime"], diff --git a/src/dotnet/ManagedCilDecoding.test.ts b/src/dotnet/ManagedCilDecoding.test.ts index d143d7491b..c1ed21130e 100644 --- a/src/dotnet/ManagedCilDecoding.test.ts +++ b/src/dotnet/ManagedCilDecoding.test.ts @@ -25,6 +25,48 @@ const managedBodyWithSections = (...sections: Buffer[]) => { .methods[0]?.body; }; +const managedBodyWithRegion = ({ + il, + flags = 0, + tryOffset = 0, + tryLength = 1, + handlerOffset, + handlerLength = 1, + extra = 0x0200_0001, +}: { + readonly il: Buffer; + readonly flags?: number; + readonly tryOffset?: number; + readonly tryLength?: number; + readonly handlerOffset: number; + readonly handlerLength?: number; + readonly extra?: number; +}) => { + const header = Buffer.alloc(12); + header.writeUInt16LE(0x300b, 0); + header.writeUInt16LE(8, 2); + header.writeUInt32LE(il.length, 4); + const section = Buffer.alloc(16); + section[0] = 0x01; + section[1] = section.length; + section.writeUInt16LE(flags, 4); + section.writeUInt16LE(tryOffset, 6); + section[8] = tryLength; + section.writeUInt16LE(handlerOffset, 9); + section[11] = handlerLength; + section.writeUInt32LE(extra, 12); + const bytes = buildManagedPeFixture({ + ilBody: Buffer.concat([ + header, + il, + Buffer.alloc((4 - (il.length % 4)) % 4), + section, + ]), + }); + return inspectManagedMembersBytes(bytes, managedPeFixtureTarget(bytes)) + .methods[0]?.body; +}; + describe("managed CIL decoding", () => { it("reads fat method header size from the full flags-and-size word", () => { const il = Buffer.from([ @@ -79,6 +121,60 @@ describe("managed CIL decoding", () => { }); }); + it("does not decode overlay bytes after the method's file-backed PE section", () => { + const original = buildManagedPeFixture(); + const bytes = Buffer.concat([original, Buffer.from([0x2a])]); + const parsed = inspectManagedMembersBytes( + original, + managedPeFixtureTarget(original), + ); + const methodRow = parsed.methods[0]?.row_offset; + expect(methodRow).toBeDefined(); + if (methodRow === undefined) return; + bytes.writeUInt32LE(0x2dff, methodRow); + bytes[0xfff] = 0x06; + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result.methods[0]?.body).toMatchObject({ + status: "malformed", + header_format: "unknown", + issue: "Method body leaves file-backed PE section data", + anchors: [], + }); + }); + + it("does not decode overlay exception sections after the file-backed PE section", () => { + const original = buildManagedPeFixture(); + const overlay = Buffer.from([0x01, 0x04, 0x00, 0x00]); + const bytes = Buffer.concat([original, overlay]); + const parsed = inspectManagedMembersBytes( + original, + managedPeFixtureTarget(original), + ); + const methodRow = parsed.methods[0]?.row_offset; + expect(methodRow).toBeDefined(); + if (methodRow === undefined) return; + const body = Buffer.alloc(16); + body.writeUInt16LE(0x301b, 0); + body.writeUInt16LE(8, 2); + body.writeUInt32LE(1, 4); + body[12] = 0x2a; + body.copy(bytes, 0xff0); + bytes.writeUInt32LE(0x2df0, methodRow); + overlay.copy(bytes, 0x1000); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + expect(result.methods[0]?.body).toMatchObject({ + status: "malformed", + issue: "Exception section header leaves artifact", + exception_regions: [], + }); + }); + it("keeps the documented decoded-CIL v1 golden vector stable", () => { const il = Buffer.from([0x00, 0x2a]); const tinyBytes = buildManagedPeFixture({ @@ -125,6 +221,218 @@ describe("managed CIL decoding", () => { }); }); +describe("managed exception region bounds", () => { + it("marks out-of-method ranges malformed and preserves the raw EH clause", () => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0x2a]), + tryOffset: 0, + tryLength: 2, + handlerOffset: 0, + }), + ).toMatchObject({ + status: "malformed", + il_size: 1, + exception_regions: [ + { + flags: 0, + try_offset: 0, + try_length: 2, + handler_offset: 0, + handler_length: 1, + class_token: "0x02000001", + }, + ], + issue: "Exception clause try range leaves the method IL body", + }); + }); + + it("uses half-open method bounds and permits zero-length ranges", () => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0x00, 0x00, 0x2a]), + tryOffset: 0, + tryLength: 1, + handlerOffset: 1, + handlerLength: 2, + }), + ).toMatchObject({ status: "present", il_size: 3, issue: null }); + expect( + managedBodyWithRegion({ + il: Buffer.from([0x2a]), + tryOffset: 0, + tryLength: 0, + handlerOffset: 0, + handlerLength: 0, + }), + ).toMatchObject({ status: "present", il_size: 1, issue: null }); + }); + + it.each([ + ["try start", { tryOffset: 1, tryLength: 4, handlerOffset: 5 }], + ["try end", { tryOffset: 0, tryLength: 1, handlerOffset: 5 }], + ["handler start", { tryLength: 5, handlerOffset: 1, handlerLength: 5 }], + ["handler end", { tryLength: 5, handlerOffset: 0, handlerLength: 1 }], + ] as const)( + "preserves clauses with a mid-instruction %s", + (_label, region) => { + const body = managedBodyWithRegion({ + il: Buffer.from([0x20, 0, 0, 0, 0, 0x2a]), + ...region, + }); + expect(body).toMatchObject({ + status: "malformed", + exception_regions: [expect.any(Object)], + issue: + "Exception clause " + + (_label.startsWith("try") ? "try" : "handler") + + " range does not align with CIL instruction boundaries", + }); + }, + ); + + it("rejects a filter start inside an instruction and retains the clause", () => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0x20, 0, 0, 0, 0, 0x2a]), + flags: 1, + tryLength: 5, + handlerOffset: 5, + extra: 1, + }), + ).toMatchObject({ + status: "malformed", + exception_regions: [expect.objectContaining({ filter_offset: 1 })], + issue: + "Exception clause filter range does not align with CIL instruction boundaries", + }); + }); +}); + +describe("managed exception region prefix boundaries", () => { + it("treats a CIL prefix chain and its opcode as one boundary", () => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0xfe, 0x12, 1, 0xfe, 0x13, 0x4a, 0x2a]), + tryOffset: 0, + tryLength: 6, + handlerOffset: 6, + }), + ).toMatchObject({ status: "present", issue: null, il_size: 7 }); + }); + + it("keeps constrained callvirt as a single region boundary", () => { + const il = Buffer.from([0xfe, 0x16, 1, 0, 0, 1, 0x6f, 1, 0, 0, 6, 0x2a]); + expect( + managedBodyWithRegion({ + il, + tryOffset: 0, + tryLength: 11, + handlerOffset: 11, + }), + ).toMatchObject({ status: "present", issue: null, il_size: 12 }); + expect( + managedBodyWithRegion({ + il, + tryOffset: 6, + tryLength: 1, + handlerOffset: 11, + }), + ).toMatchObject({ + status: "malformed", + issue: + "Exception clause try range does not align with CIL instruction boundaries", + }); + }); + + it.each([ + ["start", { tryOffset: 5, tryLength: 1, handlerOffset: 6 }], + [ + "end after first prefix", + { tryOffset: 0, tryLength: 3, handlerOffset: 6 }, + ], + [ + "end after second prefix", + { tryOffset: 0, tryLength: 5, handlerOffset: 6 }, + ], + ] as const)( + "rejects a region %s between a prefix and its opcode", + (_edge, region) => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0xfe, 0x12, 1, 0xfe, 0x13, 0x4a, 0x2a]), + ...region, + }), + ).toMatchObject({ + status: "malformed", + exception_regions: [expect.any(Object)], + issue: + "Exception clause try range does not align with CIL instruction boundaries", + }); + }, + ); +}); + +describe("managed exception region flags and filters", () => { + it("marks reserved clause flags malformed without dropping its bytes", () => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0x00, 0x2a]), + flags: 8, + tryLength: 1, + handlerOffset: 1, + }), + ).toMatchObject({ + status: "malformed", + exception_regions: [expect.objectContaining({ flags: 8 })], + issue: "Exception clause has unsupported flags 0x8", + }); + }); + + it.each([2, 4])( + "accepts the defined finally/fault clause flag %s", + (flags) => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0x00, 0x00, 0x2a]), + flags, + tryOffset: 0, + tryLength: 1, + handlerOffset: 1, + handlerLength: 2, + }), + ).toMatchObject({ status: "present", issue: null }); + }, + ); + + it("validates filter ordering", () => { + expect( + managedBodyWithRegion({ + il: Buffer.from([0x00, 0x00, 0xfe, 0x11, 0x2a]), + flags: 1, + tryOffset: 0, + tryLength: 1, + handlerOffset: 4, + extra: 1, + }), + ).toMatchObject({ status: "present", il_size: 5, issue: null }); + expect( + managedBodyWithRegion({ + il: Buffer.from([0x00, 0x00, 0x2a]), + flags: 1, + tryOffset: 0, + tryLength: 1, + handlerOffset: 1, + extra: 1, + }), + ).toMatchObject({ + status: "malformed", + issue: + "Exception clause filter must start before its handler within the method IL body", + }); + }); +}); + describe("managed exception section decoding", () => { it("marks an exception section with a partial clause as malformed", () => { const section = Buffer.alloc(17); @@ -155,18 +463,34 @@ describe("managed exception section decoding", () => { }); }); - it("reports malformed data in a chained section", () => { + it("preserves decoded clauses when a chained section has an unsupported kind", () => { const first = Buffer.alloc(16); first[0] = 0x81; first[1] = first.length; const second = Buffer.from([0x02, 0x04, 0x00, 0x00]); expect(managedBodyWithSections(first, second)).toMatchObject({ status: "malformed", - exception_regions: [], + exception_regions: [expect.objectContaining({ flags: 0 })], issue: "Unsupported method data section kind 2", }); }); + it("preserves decoded clauses when a later section has a partial clause", () => { + const first = Buffer.alloc(16); + first[0] = 0x81; + first[1] = first.length; + first.writeUInt16LE(2, 4); + const later = Buffer.alloc(17); + later[0] = 0x01; + later[1] = later.length; + + expect(managedBodyWithSections(first, later)).toMatchObject({ + status: "malformed", + exception_regions: [expect.objectContaining({ flags: 2 })], + issue: "Exception section size does not contain whole clauses", + }); + }); + it.each([ { name: "unsupported section kind", diff --git a/src/dotnet/ManagedExceptionRegionValidation.ts b/src/dotnet/ManagedExceptionRegionValidation.ts new file mode 100644 index 0000000000..e535c84f57 --- /dev/null +++ b/src/dotnet/ManagedExceptionRegionValidation.ts @@ -0,0 +1,86 @@ +import type { ManagedExceptionRegion } from "./ManagedMemberInspectorCore.js"; +import type { ExceptionRegionParseResult } from "./ManagedMemberInstructionDecoder.js"; + +const clauseFlagsIssue = (region: ManagedExceptionRegion): string | null => + [0, 1, 2, 4].includes(region.flags) + ? null + : `Exception clause has unsupported flags 0x${region.flags.toString(16)}`; + +const methodExtentIssue = ( + start: number, + length: number, + ilSize: number, + name: "try" | "handler", +): string | null => + start <= ilSize && length <= ilSize - start + ? null + : `Exception clause ${name} range leaves the method IL body`; + +const instructionBoundaryIssue = ( + start: number, + length: number, + boundaries: ReadonlySet, + name: "try" | "handler" | "filter", +): string | null => { + const end = start + length; + return boundaries.has(start) && boundaries.has(end) + ? null + : `Exception clause ${name} range does not align with CIL instruction boundaries`; +}; + +const filterOrderingIssue = ( + region: ManagedExceptionRegion, + ilSize: number, +): string | null => { + if (region.flags !== 1) return null; + const offset = region.filter_offset; + return offset !== null && offset < ilSize && offset < region.handler_offset + ? null + : "Exception clause filter must start before its handler within the method IL body"; +}; + +const clauseIssue = ( + region: ManagedExceptionRegion, + ilSize: number, + boundaries: ReadonlySet, +): string | null => + clauseFlagsIssue(region) ?? + methodExtentIssue(region.try_offset, region.try_length, ilSize, "try") ?? + instructionBoundaryIssue( + region.try_offset, + region.try_length, + boundaries, + "try", + ) ?? + methodExtentIssue( + region.handler_offset, + region.handler_length, + ilSize, + "handler", + ) ?? + instructionBoundaryIssue( + region.handler_offset, + region.handler_length, + boundaries, + "handler", + ) ?? + (region.flags === 1 && region.filter_offset !== null + ? instructionBoundaryIssue(region.filter_offset, 0, boundaries, "filter") + : null) ?? + filterOrderingIssue(region, ilSize); + +/** Validate EH extents and instruction boundaries while retaining each clause. */ +export const validateExceptionRegionRanges = ( + parsed: ExceptionRegionParseResult, + ilSize: number, + instructionOffsets: readonly number[], +): ExceptionRegionParseResult => { + if (parsed.status === "malformed") return parsed; + const boundaries = new Set([...instructionOffsets, ilSize]); + for (const region of parsed.regions) { + const issue = clauseIssue(region, ilSize, boundaries); + if (issue !== null) + return { status: "malformed", regions: parsed.regions, issue }; + } + return parsed; +}; diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index 7d81245607..59c3adcf62 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -7,6 +7,28 @@ import { } from "./ManagedPe.fixture.js"; describe("managed member inspection", () => { + it("keeps metadata handles distinct from calls and field accesses", () => { + const il = Buffer.from([ + 0xd0, 0x01, 0, 0, 0x06, 0x26, 0xd0, 0x01, 0, 0, 0x04, 0x26, 0x28, 0x01, 0, + 0, 0x0a, 0x7b, 0x01, 0, 0, 0x04, 0x26, 0x2a, + ]); + const bytes = buildManagedPeFixture({ + ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]), + }); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.methods[0]?.body.anchors.map(({ opcode }) => opcode)).toEqual( + ["ldtoken", "ldtoken", "call", "ldfld"], + ); + expect(result.call_edges.map(({ opcode }) => opcode)).toEqual(["call"]); + expect(result.field_accesses.map(({ opcode }) => opcode)).toEqual([ + "ldfld", + ]); + }); + it("inspects metadata members, signatures, CIL hashes, call edges, and field anchors", () => { const bytes = buildManagedPeFixture(); const result = inspectManagedMembersBytes( @@ -111,6 +133,115 @@ describe("managed member inspection", () => { }); }); +describe("managed member reference bounds", () => { + it("retains members and reports an out-of-range TypeDef coded index as partial", () => { + const bytes = buildManagedPeFixture({ extendsRaw: 12 }); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.types[0]).toMatchObject({ + token: "0x02000001", + extends_token: null, + }); + expect(result.fields).toHaveLength(1); + expect(result.methods).toHaveLength(1); + expect(result.metadata.status).toBe("partial"); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + { + code: "invalid-row", + scope: "metadata.TypeDef:0x02000001", + detail: expect.stringContaining( + "TypeDef Extends coded index 0xc is invalid: coded index selects row 3 in table 2, which has 1 rows", + ), + }, + ], + }); + expect(result.coverage.issues[0]?.offset).toBe( + (result.types[0]?.row_offset ?? 0) + 8, + ); + }); + + it("retains a MemberRef with an out-of-range parent as an unknown partial reference", () => { + const bytes = buildManagedPeFixture({ memberRefParentRaw: 40 }); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.member_refs[0]).toMatchObject({ + token: "0x0a000001", + parent_token: null, + name: ".ctor", + }); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + { + code: "invalid-row", + scope: "metadata.MemberRef:0x0a000001", + detail: expect.stringContaining( + "MemberRef parent coded index 0x28 is invalid: coded index selects row 5 in table 2, which has 1 rows", + ), + }, + ], + }); + expect(result.coverage.issues[0]?.offset).toBe( + result.member_refs[0]?.row_offset, + ); + }); + + it("reports a null required MemberRef parent without dropping the reference", () => { + const bytes = buildManagedPeFixture({ memberRefParentRaw: 0 }); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.member_refs[0]).toMatchObject({ + token: "0x0a000001", + parent_token: null, + }); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + detail: + "MemberRef parent coded index 0x0 is null, but the Class column must reference a row", + }), + ], + }); + }); + + it("decodes deeply nested supported pointer signatures without recursion", () => { + const depth = 8_000; + const bytes = buildManagedPeFixture({ + fieldSignature: Buffer.concat([ + Buffer.from([0x06]), + Buffer.alloc(depth, 0x0f), + Buffer.from([0x08]), + ]), + }); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.fields[0]?.signature).toMatchObject({ + kind: "field", + parse_status: "decoded", + field_type: `${"i4"}${"*".repeat(depth)}`, + issue: null, + }); + expect(result.methods[0]?.body.status).toBe("present"); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); + }); +}); + it.each([ Buffer.alloc(0), Buffer.from([0]), diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 8e3b6ef3d8..23a2435ea6 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -116,9 +116,11 @@ export const inspectManagedMembersBytes = ( const { layout, inventory } = readMemberInventory(bytes, pe); issues.push(...inventory.issues); const ranges = typeRanges(bytes, layout); - const types = parseTypes(bytes, layout, ranges); + const parsedTypes = parseTypes(bytes, layout, ranges); + issues.push(...parsedTypes.issues); const fields = parseFields(bytes, layout, ranges); const memberRefs = parseMemberRefs(bytes, layout); + issues.push(...memberRefs.issues); const methods = parseMethods({ bytes, layout, @@ -150,7 +152,7 @@ export const inspectManagedMembersBytes = ( requires_artifact_sha256: target.sha256, requires_mvid: inventory.module?.mvid ?? null, }, - types, + types: parsedTypes.types, fields: fields.fields, methods: methods.methods, member_refs: memberRefs.refs, diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 227a93cc92..db17acdc17 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -2,7 +2,10 @@ import { managedFailure, ManagedReaderFailure, } from "./ManagedReaderFailure.js"; -import type { ManagedMemberInspection } from "../domain/managed/managedArtifact.js"; +import type { + ManagedMemberInspection, + ManagedParseIssue, +} from "../domain/managed/managedArtifact.js"; import { type ManagedMetadataLayout, type MetadataTableLayout, @@ -10,6 +13,7 @@ import { import { metadataRowCursor, metadataCodedToken, + metadataCodedTokenInvalidReason, metadataToken, readMetadataString, sha256Bytes, @@ -190,11 +194,15 @@ export const parseTypes = ( bytes: Buffer, layout: ManagedMetadataLayout, ranges: readonly TypeRange[], -): readonly ManagedType[] => { +): { + readonly types: readonly ManagedType[]; + readonly issues: readonly ManagedParseIssue[]; +} => { const typeTable = layout.table(2); const fields = layout.table(4); const methods = layout.table(6); const items: ManagedType[] = []; + const issues: ManagedParseIssue[] = []; for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) { const cursor = metadataRowCursor(bytes, layout, 2, row); const flags = cursor.readUInt32(); @@ -210,6 +218,7 @@ export const parseTypes = ( cursor.readIndex(layout.stringIndexSize), layout.strings.size, ); + const extendsOffset = cursor.offset; const extendsRaw = cursor.readIndex(layout.codedIndexSize("TypeDefOrRef")); const fieldStart = cursor.readIndex(layout.tableIndexSize(4)); const methodStart = cursor.readIndex(layout.tableIndexSize(6)); @@ -220,6 +229,19 @@ export const parseTypes = ( methodStart, nextRange?.methodStart ?? 0, ); + const extendsReason = metadataCodedTokenInvalidReason( + extendsRaw, + 2, + [2, 1, 27], + layout.rowCounts, + ); + if (extendsReason !== null) + issues.push({ + code: "invalid-row", + scope: `metadata.TypeDef:${metadataToken(2, row)}`, + offset: extendsOffset, + detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`, + }); items.push({ token: metadataToken(2, row), row_offset: cursor.start, @@ -227,7 +249,12 @@ export const parseTypes = ( name, full_name: fullName(namespace, name), flags, - extends_token: metadataCodedToken(extendsRaw, 2, [2, 1, 27]), + extends_token: metadataCodedToken( + extendsRaw, + 2, + [2, 1, 27], + layout.rowCounts, + ), field_list: { first_row: fieldRange.first, last_row: fieldRange.last, @@ -240,7 +267,7 @@ export const parseTypes = ( }, }); } - return items; + return { types: items, issues }; }; const readCompressed = ( @@ -301,55 +328,48 @@ const ELEMENT_TYPES = new Map([ [0x1c, "object"], ]); -const ELEMENT_TYPE_SUFFIXES = new Map([ - [0x0f, "*"], - [0x10, "&"], -]); - const readTypeSignature = ( blob: Buffer, offset: number, ): { readonly value: string; readonly next: number } => { - const kind = blob[offset]; + const suffixes: string[] = []; + let cursor = offset; + let kind = blob[cursor]; + while (kind === 0x0f || kind === 0x10 || kind === 0x1d) { + suffixes.push(kind === 0x0f ? "*" : kind === 0x10 ? "&" : "[]"); + cursor += 1; + kind = blob[cursor]; + } if (kind === undefined) throw managedFailure( "invalid-blob", "signature", "truncated type signature", - offset, + cursor, ); const named = ELEMENT_TYPES.get(kind); - if (named !== undefined) return { value: named, next: offset + 1 }; - const suffix = ELEMENT_TYPE_SUFFIXES.get(kind); - if (suffix !== undefined) { - const inner = readTypeSignature(blob, offset + 1); - return { value: `${inner.value}${suffix}`, next: inner.next }; + let value: string; + let next: number; + if (named !== undefined) { + value = named; + next = cursor + 1; + } else if (kind === 0x11 || kind === 0x12) { + const token = readCompressed(blob, cursor + 1); + value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`; + next = token.next; + } else if (kind === 0x1e || kind === 0x13) { + const variable = readCompressed(blob, cursor + 1); + value = `${kind === 0x1e ? "mvar" : "var"}:${String(variable.value)}`; + next = variable.next; + } else { + throw managedFailure( + "unsupported-signature", + "signature", + `unsupported element type 0x${kind.toString(16)}`, + cursor, + ); } - if (kind === 0x11 || kind === 0x12) { - const token = readCompressed(blob, offset + 1); - return { - value: `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`, - next: token.next, - }; - } - if (kind === 0x1d) { - const inner = readTypeSignature(blob, offset + 1); - return { value: `${inner.value}[]`, next: inner.next }; - } - if (kind === 0x1e) { - const variable = readCompressed(blob, offset + 1); - return { value: `mvar:${String(variable.value)}`, next: variable.next }; - } - if (kind === 0x13) { - const variable = readCompressed(blob, offset + 1); - return { value: `var:${String(variable.value)}`, next: variable.next }; - } - throw managedFailure( - "unsupported-signature", - "signature", - `unsupported element type 0x${kind.toString(16)}`, - offset, - ); + return { value: value + suffixes.reverse().join(""), next }; }; const callingConvention = (value: number): string => { diff --git a/src/dotnet/ManagedMemberInstructionDecoder.ts b/src/dotnet/ManagedMemberInstructionDecoder.ts index ea61cc289f..7c8a11a054 100644 --- a/src/dotnet/ManagedMemberInstructionDecoder.ts +++ b/src/dotnet/ManagedMemberInstructionDecoder.ts @@ -9,6 +9,14 @@ const opcode = ( ): { readonly name: string; readonly operand: string } | undefined => OPCODES.get(code); +const PREFIX_OPCODES = new Set([ + "unaligned.", + "volatile.", + "tail.", + "constrained.", + "readonly.", +]); + const OPCODES = new Map< number, { readonly name: string; readonly operand: string } @@ -379,12 +387,15 @@ export const decodeInstructions = ( il: Buffer, ): { readonly parsed: readonly ParsedInstruction[]; + readonly instructionStarts: readonly number[]; readonly count: number; readonly truncated: number; readonly issue: string | null; } => { const parsed: ParsedInstruction[] = []; + const instructionStarts: number[] = []; let offset = 0; + let prefixStart: number | null = null; let issue: string | null = null; try { while (offset < il.length) { @@ -403,6 +414,12 @@ export const decodeInstructions = ( break; } offset = operand.next; + if (PREFIX_OPCODES.has(descriptor.name)) { + prefixStart ??= start; + } else { + instructionStarts.push(prefixStart ?? start); + prefixStart = null; + } parsed.push({ offset: start, opcode: descriptor.name, @@ -415,7 +432,7 @@ export const decodeInstructions = ( cause instanceof Error ? cause.message : "Instruction decode failed"; } const truncated = offset < il.length && issue === null ? 1 : 0; - return { parsed, count: parsed.length, truncated, issue }; + return { parsed, instructionStarts, count: parsed.length, truncated, issue }; }; export const parseExceptionRegions = ( @@ -435,12 +452,14 @@ export const parseExceptionRegions = ( ) return malformedExceptionRegions( "Exception section header leaves artifact", + regions, ); const kind = bytes.readUInt8(sectionOffset); if ((kind & 0x3f) !== 1) return malformedExceptionRegions( `Unsupported method data section kind ${String(kind & 0x3f)}`, + regions, ); const fat = (kind & 0x40) !== 0; const size = fat @@ -456,10 +475,12 @@ export const parseExceptionRegions = ( ) return malformedExceptionRegions( "Exception section size leaves artifact", + regions, ); if ((size - 4) % clauseSize !== 0) return malformedExceptionRegions( "Exception section size does not contain whole clauses", + regions, ); if ( !fat && @@ -467,6 +488,7 @@ export const parseExceptionRegions = ( ) return malformedExceptionRegions( "Small exception section reserved bytes are nonzero", + regions, ); const count = (size - 4) / clauseSize; @@ -478,24 +500,21 @@ export const parseExceptionRegions = ( return { status: "complete", regions }; }; -interface ExceptionRegionParseSuccess { - readonly status: "complete"; - readonly regions: ManagedExceptionRegion[]; -} - -interface ExceptionRegionParseFailure { - readonly status: "malformed"; - readonly regions: []; - readonly issue: string; -} - -type ExceptionRegionParseResult = - | ExceptionRegionParseSuccess - | ExceptionRegionParseFailure; +export type ExceptionRegionParseResult = + | { + readonly status: "complete"; + readonly regions: ManagedExceptionRegion[]; + } + | { + readonly status: "malformed"; + readonly regions: ManagedExceptionRegion[]; + readonly issue: string; + }; const malformedExceptionRegions = ( issue: string, -): ExceptionRegionParseFailure => ({ status: "malformed", regions: [], issue }); + regions: ManagedExceptionRegion[] = [], +): ExceptionRegionParseResult => ({ status: "malformed", regions, issue }); const readExceptionClauses = ( bytes: Buffer, diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index d948e54f76..e9cb8019ce 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -1,4 +1,5 @@ import type { ManagedPeLayout } from "./ManagedPeReader.js"; +import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { createDeclaringTypeLookup, @@ -17,6 +18,7 @@ import { metadataToken, metadataRowCursor, metadataCodedToken, + metadataCodedTokenInvalidReason, readMetadataBlob, readMetadataString, } from "./ManagedMetadataHeaps.js"; @@ -71,9 +73,11 @@ export const parseMemberRefs = ( ): { readonly refs: readonly ManagedMemberRef[]; readonly core: ReadonlyMap; + readonly issues: readonly ManagedParseIssue[]; } => { const refs: ManagedMemberRef[] = []; const core = new Map(); + const issues: ManagedParseIssue[] = []; const table = layout.table(10); for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) { const cursor = metadataRowCursor(bytes, layout, 10, row); @@ -93,16 +97,37 @@ export const parseMemberRefs = ( layout.blob.size, ); const token = metadataToken(10, row); + const parentReason = metadataCodedTokenInvalidReason( + parentRaw, + 3, + [2, 1, 26, 6, 27], + layout.rowCounts, + ); + if (parentReason !== null || parentRaw === 0) + issues.push({ + code: "invalid-row", + scope: `metadata.MemberRef:${token}`, + offset: cursor.start, + detail: + parentReason === null + ? "MemberRef parent coded index 0x0 is null, but the Class column must reference a row" + : `MemberRef parent coded index 0x${parentRaw.toString(16)} is invalid: ${parentReason}`, + }); refs.push({ token, row_offset: cursor.start, - parent_token: metadataCodedToken(parentRaw, 3, [2, 1, 26, 6, 27]), + parent_token: metadataCodedToken( + parentRaw, + 3, + [2, 1, 26, 6, 27], + layout.rowCounts, + ), name, signature: signature(sig), }); core.set(token, { token, name }); } - return { refs, core }; + return { refs, core, issues }; }; interface ParseMethodsInput { diff --git a/src/dotnet/ManagedMetadataHandles.test.ts b/src/dotnet/ManagedMetadataHandles.test.ts deleted file mode 100644 index e5fc60fa5d..0000000000 --- a/src/dotnet/ManagedMetadataHandles.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect, it } from "vitest"; -import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; -import { - buildManagedPeFixture, - managedPeFixtureTarget, -} from "./ManagedPe.fixture.js"; - -it("retains metadata handles without reporting calls or field accesses", () => { - const il = Buffer.from([ - 0xd0, 0x01, 0, 0, 0x06, 0x26, 0xd0, 0x01, 0, 0, 0x04, 0x26, 0x28, 0x01, 0, - 0, 0x0a, 0x7b, 0x01, 0, 0, 0x04, 0x26, 0x2a, - ]); - const bytes = buildManagedPeFixture({ - ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]), - }); - const result = inspectManagedMembersBytes( - bytes, - managedPeFixtureTarget(bytes), - ); - expect( - result.methods[0]?.body.anchors.map((anchor) => anchor.opcode), - ).toEqual(["ldtoken", "ldtoken", "call", "ldfld"]); - expect(result.call_edges.map((edge) => edge.opcode)).toEqual(["call"]); - expect(result.field_accesses.map((access) => access.opcode)).toEqual([ - "ldfld", - ]); -}); diff --git a/src/dotnet/ManagedMetadataHeaps.ts b/src/dotnet/ManagedMetadataHeaps.ts index 9994a41a4f..ab59caa213 100644 --- a/src/dotnet/ManagedMetadataHeaps.ts +++ b/src/dotnet/ManagedMetadataHeaps.ts @@ -253,10 +253,32 @@ export const metadataCodedToken = ( raw: number, bits: number, tables: readonly (number | undefined)[], + rowCounts: readonly number[], ): string | null => { if (raw === 0) return null; const tag = raw & (2 ** bits - 1); const row = Math.floor(raw / 2 ** bits); const table = tables[tag]; - return table === undefined || row === 0 ? null : metadataToken(table, row); + return table === undefined || row === 0 || row > (rowCounts[table] ?? 0) + ? null + : metadataToken(table, row); +}; + +/** Explain why a non-null coded index cannot name an admitted metadata row. */ +export const metadataCodedTokenInvalidReason = ( + raw: number, + bits: number, + tables: readonly (number | undefined)[], + rowCounts: readonly number[], +): string | null => { + if (raw === 0) return null; + const tag = raw & (2 ** bits - 1); + const row = Math.floor(raw / 2 ** bits); + const table = tables[tag]; + if (table === undefined) return `coded index tag ${tag} is not defined`; + if (row === 0) return `coded index selects row zero in table ${table}`; + const count = rowCounts[table] ?? 0; + return row > count + ? `coded index selects row ${row} in table ${table}, which has ${count} rows` + : null; }; diff --git a/src/dotnet/ManagedMetadataInventory.ts b/src/dotnet/ManagedMetadataInventory.ts index f0eadaaa09..a347fe6207 100644 --- a/src/dotnet/ManagedMetadataInventory.ts +++ b/src/dotnet/ManagedMetadataInventory.ts @@ -73,17 +73,48 @@ const uniqueIssues = ( const heapExtent = (layout: ManagedMetadataLayout): number => Math.max(layout.strings.size, layout.blob.size); +const validateIdentityTableCounts = ( + layout: ManagedMetadataLayout, + issues: ManagedParseIssue[], +): void => { + const moduleRows = layout.table(0)?.rowCount ?? 0; + if (moduleRows !== 1) + issues.push({ + code: "invalid-row", + scope: "metadata.Module", + offset: layout.table(0)?.offset ?? null, + detail: `Module table must contain exactly one row; found ${String(moduleRows)}`, + }); + + const assemblyRows = layout.table(32)?.rowCount ?? 0; + if (assemblyRows > 1) + issues.push({ + code: "invalid-row", + scope: "metadata.Assembly", + offset: layout.table(32)?.offset ?? null, + detail: `Assembly table can contain at most one row; found ${String(assemblyRows)}`, + }); +}; + const readReferences = ( bytes: Buffer, layout: ManagedMetadataLayout, issues: ManagedParseIssue[], -): readonly AssemblyReference[] => - readRows(layout.table(35), (row) => +): { + readonly references: readonly AssemblyReference[]; + readonly referenceNames: readonly string[]; +} => { + const references = readRows(layout.table(35), (row) => safeRead( () => readAssemblyReference(bytes, layout, row, heapExtent(layout)), issues, ), ); + return { + references, + referenceNames: [...new Set(references.map(({ name }) => name))].sort(), + }; +}; const readResources = ({ bytes, @@ -114,53 +145,27 @@ const readAttributes = ( bytes: Buffer, layout: ManagedMetadataLayout, issues: ManagedParseIssue[], -): readonly CustomAttribute[] => - readRows(layout.table(12), (row) => +): { + readonly attributes: readonly CustomAttribute[]; + readonly targetFrameworks: readonly string[]; +} => { + const attributes = readRows(layout.table(12), (row) => safeRead( () => readCustomAttribute(bytes, layout, row, heapExtent(layout)), issues, ), ); - -const collectReferenceNames = ( - bytes: Buffer, - layout: ManagedMetadataLayout, - issues: ManagedParseIssue[], -): readonly string[] => { - const names: string[] = []; - const referenceTable = layout.table(35); - for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) { - const reference = safeRead( - () => readAssemblyReference(bytes, layout, row, heapExtent(layout)), - issues, - ); - if (reference !== undefined) names.push(reference.name); - } - return names; -}; - -const collectTargetFrameworks = ( - bytes: Buffer, - layout: ManagedMetadataLayout, - issues: ManagedParseIssue[], -): readonly string[] => { - const targetFrameworks: string[] = []; - const attributeTable = layout.table(12); - for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) { - const attribute = safeRead( - () => readCustomAttribute(bytes, layout, row, heapExtent(layout)), - issues, - ); + const targetFrameworks = new Set(); + for (const attribute of attributes) { if ( - attribute?.parent_token === metadataToken(32, 1) && + attribute.parent_token === metadataToken(32, 1) && attribute.type_name === "System.Runtime.Versioning.TargetFrameworkAttribute" && - attribute.decoded_fixed_string !== null && - !targetFrameworks.includes(attribute.decoded_fixed_string) + attribute.decoded_fixed_string !== null ) - targetFrameworks.push(attribute.decoded_fixed_string); + targetFrameworks.add(attribute.decoded_fixed_string); } - return targetFrameworks; + return { attributes, targetFrameworks: [...targetFrameworks].sort() }; }; /** Inventory identity tables without CLR reflection or execution. */ @@ -170,27 +175,30 @@ export const readManagedMetadataInventory = ( resourceDirectory: ManagedResourceDirectory | null, ): ManagedMetadataInventory => { const issues: ManagedParseIssue[] = []; + validateIdentityTableCounts(layout, issues); const module = safeRead(() => readModule(bytes, layout, heapExtent(layout)), issues) ?? null; const assembly = safeRead(() => readAssembly(bytes, layout, heapExtent(layout)), issues) ?? null; - const references = readReferences(bytes, layout, issues); + const { references, referenceNames } = readReferences(bytes, layout, issues); const resources = readResources({ bytes, layout, resourceDirectory, issues, }); - const attributes = readAttributes(bytes, layout, issues); - const referenceNames = collectReferenceNames(bytes, layout, issues); - const targetFrameworks = collectTargetFrameworks(bytes, layout, issues); + const { attributes, targetFrameworks } = readAttributes( + bytes, + layout, + issues, + ); return { module, assembly, - targetFrameworks: [...targetFrameworks].sort(), - referenceNames: [...new Set(referenceNames)].sort(), + targetFrameworks, + referenceNames, references, resources, attributes, diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 836c9a376b..596cb519c6 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -9,6 +9,7 @@ import { import { metadataRowCursor, metadataCodedToken, + metadataCodedTokenInvalidReason, metadataToken, readMetadataBlob, readMetadataGuid, @@ -365,6 +366,7 @@ export const readCustomAttribute = ( const parentRaw = cursor.readIndex( layout.codedIndexSize("HasCustomAttribute"), ); + const typeOffset = cursor.offset; const typeRaw = cursor.readIndex( layout.codedIndexSize("CustomAttributeType"), ); @@ -381,23 +383,48 @@ export const readCustomAttribute = ( 6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42, 44, 43, ], + layout.rowCounts, + ); + const parentReason = metadataCodedTokenInvalidReason( + parentRaw, + 5, + [ + 6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42, + 44, 43, + ], + layout.rowCounts, ); if (parent === null) throw managedFailure( "invalid-row", - "metadata.CustomAttribute", - "CustomAttribute parent coded index is invalid", + `metadata.CustomAttribute:${metadataToken(12, row)}`, + `CustomAttribute parent coded index 0x${parentRaw.toString(16)} is invalid${parentReason === null ? " because the required parent is null" : `: ${parentReason}`}`, cursor.start, ); + const typeReason = metadataCodedTokenInvalidReason( + typeRaw, + 3, + [undefined, undefined, 6, 10], + layout.rowCounts, + ); + if (typeReason !== null || typeRaw === 0) + throw managedFailure( + "invalid-row", + `metadata.CustomAttribute:${metadataToken(12, row)}`, + typeReason === null + ? "CustomAttribute constructor coded index 0x0 is null, but Type must reference a MethodDef or MemberRef row" + : `CustomAttribute constructor coded index 0x${typeRaw.toString(16)} is invalid: ${typeReason}`, + typeOffset, + ); const typeName = attributeTypeName(bytes, layout, typeRaw, heapExtent); return { parent_token: parent, - constructor_token: metadataCodedToken(typeRaw, 3, [ - undefined, - undefined, - 6, - 10, - ]), + constructor_token: metadataCodedToken( + typeRaw, + 3, + [undefined, undefined, 6, 10], + layout.rowCounts, + ), type_name: typeName, value_length: value.length, value_sha256: sha256Bytes(value), @@ -434,6 +461,7 @@ export const readResource = ({ cursor.readIndex(layout.stringIndexSize), Math.max(layout.strings.size, layout.blob.size), ); + const implementationOffset = cursor.offset; const implementationRaw = cursor.readIndex( layout.codedIndexSize("Implementation"), ); @@ -441,7 +469,21 @@ export const readResource = ({ implementationRaw, 2, [38, 35, 39], + layout.rowCounts, ); + const implementationReason = metadataCodedTokenInvalidReason( + implementationRaw, + 2, + [38, 35, 39], + layout.rowCounts, + ); + if (implementationReason !== null) + issues.push({ + code: "invalid-row", + scope: `metadata.ManifestResource:${metadataToken(40, row)}`, + offset: implementationOffset, + detail: `ManifestResource implementation coded index 0x${implementationRaw.toString(16)} is invalid: ${implementationReason}`, + }); let dataLength: number | null = null; let dataSha256: string | null = null; if (implementationRaw === 0) { diff --git a/src/dotnet/ManagedMethodBodyReader.ts b/src/dotnet/ManagedMethodBodyReader.ts index b8f0d29485..fd1c5e6427 100644 --- a/src/dotnet/ManagedMethodBodyReader.ts +++ b/src/dotnet/ManagedMethodBodyReader.ts @@ -4,6 +4,7 @@ import { decodeInstructions, parseExceptionRegions, } from "./ManagedMemberInstructionDecoder.js"; +import { validateExceptionRegionRanges } from "./ManagedExceptionRegionValidation.js"; import { sha256Bytes } from "./ManagedMetadataHeaps.js"; const bodyStatus = ( @@ -60,6 +61,19 @@ const readMethodBodyHeader = ( }; }; +const readExceptionRegions = ( + bytes: Buffer, + sectionOffset: number, + methodEnd: number, + ilSize: number, + instructionOffsets: readonly number[], +): ReturnType => + validateExceptionRegionRanges( + parseExceptionRegions(bytes, sectionOffset, methodEnd), + ilSize, + instructionOffsets, + ); + /** Decode admitted managed CIL, retaining unavailable implementation metadata as partial. */ export const methodBody = ( bytes: Buffer, @@ -82,7 +96,13 @@ export const methodBody = ( ); try { const offset = pe.rvaToOffset(rva, 1, "method.body"); + const methodExtent = pe.rvaAvailableBytes(rva, "method.body"); const header = readMethodBodyHeader(bytes, offset); + if ( + header.size > methodExtent || + header.ilSize > methodExtent - header.size + ) + throw new RangeError("Method body leaves file-backed PE section data"); const ilOffset = offset + header.size; if (ilOffset > bytes.length - header.ilSize) throw new RangeError("Method IL bytes leave artifact"); @@ -114,7 +134,13 @@ export const methodBody = ( const sectionOffset = (methodEnd + 3) & ~3; const exceptionRegions = header.format === "fat" && (header.flags & 8) !== 0 - ? parseExceptionRegions(bytes, sectionOffset, bytes.length) + ? readExceptionRegions( + bytes, + sectionOffset, + offset + methodExtent, + header.ilSize, + decoded.instructionStarts, + ) : null; const status = bodyStatus( decoded.issue, diff --git a/src/dotnet/ManagedNativeBoundaryHelpers.ts b/src/dotnet/ManagedNativeBoundaryHelpers.ts index 5ef2e3bbb7..3ba7d81c4e 100644 --- a/src/dotnet/ManagedNativeBoundaryHelpers.ts +++ b/src/dotnet/ManagedNativeBoundaryHelpers.ts @@ -8,6 +8,7 @@ import { type ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { metadataRowCursor, metadataCodedToken, + metadataCodedTokenInvalidReason, metadataToken, readMetadataString, } from "./ManagedMetadataHeaps.js"; @@ -127,6 +128,7 @@ interface ParseImplMapsContext { readonly heapExtent: number; readonly modules: readonly ModuleRef[]; readonly members: ReadonlyMap; + readonly issues: ManagedParseIssue[]; } export const parseImplMaps = ({ @@ -135,6 +137,7 @@ export const parseImplMaps = ({ heapExtent, modules, members, + issues, }: ParseImplMapsContext): readonly NativeImport[] => { const moduleNames = new Map(modules.map((module) => [module.token, module])); const imports: NativeImport[] = []; @@ -142,10 +145,31 @@ export const parseImplMaps = ({ for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) { const cursor = metadataRowCursor(bytes, layout, 28, row); const mappingFlags = cursor.readUInt16(); - const memberToken = metadataCodedToken( - cursor.readIndex(layout.codedIndexSize("MemberForwarded")), + const memberForwardedOffset = cursor.offset; + const memberForwardedRaw = cursor.readIndex( + layout.codedIndexSize("MemberForwarded"), + ); + const memberForwardedReason = metadataCodedTokenInvalidReason( + memberForwardedRaw, 1, [4, 6], + layout.rowCounts, + ); + if (memberForwardedReason !== null || memberForwardedRaw === 0) + issues.push({ + code: "invalid-row", + scope: `metadata.ImplMap:${metadataToken(28, row)}`, + offset: memberForwardedOffset, + detail: + memberForwardedReason === null + ? "ImplMap MemberForwarded coded index 0x0 is null, but the column must reference a Field or MethodDef row" + : `ImplMap MemberForwarded coded index 0x${memberForwardedRaw.toString(16)} is invalid: ${memberForwardedReason}`, + }); + const memberToken = metadataCodedToken( + memberForwardedRaw, + 1, + [4, 6], + layout.rowCounts, ); const importName = readMetadataString( bytes, diff --git a/src/dotnet/ManagedNativeBoundaryInspector.test.ts b/src/dotnet/ManagedNativeBoundaryInspector.test.ts index 70c49248d4..1f2a702ef1 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.test.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; +import { createHash } from "node:crypto"; + +import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js"; import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js"; import { @@ -68,11 +71,74 @@ describe("managed native boundaries", () => { ready_to_run: true, mixed_mode_or_native_header: true, }); + expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); expect(result.limitations).toContain( "P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.", ); }); +}); +describe("managed native boundary coded indexes", () => { + it("retains P/Invoke imports with an invalid MemberForwarded index as partial evidence", () => { + const bytes = buildManagedPeFixture({ + pinvoke: { memberForwardedRaw: 5 }, + }); + const result = inspectManagedNativeBoundariesBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.pinvoke_imports).toMatchObject([ + { + member_token: null, + member_kind: "unknown", + member_name: null, + import_name: "MessageBoxW", + }, + ]); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + scope: "metadata.ImplMap:0x1c000001", + detail: expect.stringContaining( + "coded index 0x5 is invalid: coded index selects row 2 in table 6, which has 1 rows", + ), + }), + ], + }); + expect(result.coverage.issues[0]?.offset).toBe( + (result.pinvoke_imports[0]?.row_offset ?? 0) + 2, + ); + }); + + it("reports null for the required MemberForwarded index", () => { + const bytes = buildManagedPeFixture({ + pinvoke: { memberForwardedRaw: 0 }, + }); + const result = inspectManagedNativeBoundariesBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.pinvoke_imports).toMatchObject([ + { member_token: null, member_kind: "unknown" }, + ]); + expect(result.coverage).toMatchObject({ + state: "partial", + issues: [ + expect.objectContaining({ + code: "invalid-row", + detail: + "ImplMap MemberForwarded coded index 0x0 is null, but the column must reference a Field or MethodDef row", + }), + ], + }); + }); +}); + +describe("managed native boundary inspection", () => { it("returns complete member inventories inline and reports unavailable metadata", () => { const bytes = buildManagedPeFixture(); const paged = inspectManagedMembersBytes( @@ -158,3 +224,34 @@ describe("managed native boundaries", () => { ); }); }); + +describe("managed PE fixture layout", () => { + it("keeps ReadyToRun metadata separate from expanded metadata and resources", () => { + const resourceData = Buffer.alloc(64 * 1024, 0x5a); + const bytes = buildManagedPeFixture({ + readyToRun: true, + references: Array.from( + { length: 2_200 }, + (_, index) => `Reference.${String(index).padStart(4, "0")}`, + ), + resourceData, + }); + const target = managedPeFixtureTarget(bytes); + const artifact = inspectManagedArtifactBytes(bytes, target); + const boundaries = inspectManagedNativeBoundariesBytes(bytes, target); + + expect(artifact.coverage).toMatchObject({ state: "complete", issues: [] }); + expect(artifact.references).toHaveLength(2_200); + expect(artifact.resources[0]).toMatchObject({ + data_length: resourceData.length, + data_sha256: createHash("sha256").update(resourceData).digest("hex"), + }); + expect(boundaries.cli_native).toMatchObject({ + ready_to_run_signature: true, + managed_native_header_size: 4, + }); + expect(boundaries.cli_native.managed_native_header_rva).toBeGreaterThan( + 0x2700, + ); + }); +}); diff --git a/src/dotnet/ManagedNativeBoundaryInspector.ts b/src/dotnet/ManagedNativeBoundaryInspector.ts index a0e58f3c05..2bed713ba2 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.ts @@ -156,6 +156,7 @@ export const inspectManagedNativeBoundariesBytes = ( }); } const heapExtent = Math.max(layout.strings.size, layout.blob.size); + const issues = [...inventory.issues]; const moduleRefs = parseModuleRefs(bytes, layout, heapExtent); const members = new Map([ ...parseFields(bytes, layout, heapExtent), @@ -167,6 +168,7 @@ export const inspectManagedNativeBoundariesBytes = ( heapExtent, modules: moduleRefs, members, + issues, }); const pinvokeTokens = new Set( imports @@ -187,6 +189,6 @@ export const inspectManagedNativeBoundariesBytes = ( imports, implementations, native: cliNative(pe), - issues: [], + issues, }); }; diff --git a/src/dotnet/ManagedPe.fixture.ts b/src/dotnet/ManagedPe.fixture.ts index 076a4cd8ec..05d035ab63 100644 --- a/src/dotnet/ManagedPe.fixture.ts +++ b/src/dotnet/ManagedPe.fixture.ts @@ -5,22 +5,31 @@ import type { BinaryTarget } from "../domain/binaryTarget.js"; interface ManagedPeFixtureOptions { readonly cliFlags?: number; readonly corruptMetadataSignature?: boolean; + readonly customAttributeTypeRaw?: number; + readonly extendsRaw?: number; readonly fieldName?: string; readonly fieldSignature?: Buffer; readonly ilBody?: Buffer; + readonly malformedAssemblyReferenceRows?: readonly number[]; + readonly malformedCustomAttributeRows?: readonly number[]; readonly metadataValidMaskExtra?: bigint; readonly methodName?: string; readonly methodSignature?: Buffer; + readonly moduleRowCount?: number; + readonly assemblyRowCount?: number; + readonly memberRefParentRaw?: number; readonly mvid?: Buffer; readonly pinvoke?: { readonly importName?: string; + readonly memberForwardedRaw?: number; readonly mappingFlags?: number; readonly moduleName?: string; }; readonly readyToRun?: boolean; readonly references?: readonly string[]; + readonly resourceImplementationRaw?: number; readonly resourceData?: Buffer; - readonly targetFramework?: string; + readonly targetFramework?: string | null; readonly typeName?: string; readonly typeNamespace?: string; } @@ -35,6 +44,10 @@ class StringHeap { readonly #chunks: Buffer[] = [Buffer.from([0])]; #size = 1; + get size(): number { + return this.#size; + } + add(value: string): number { if (value.length === 0) return 0; const index = this.#size; @@ -53,13 +66,26 @@ class BlobHeap { readonly #chunks: Buffer[] = [Buffer.from([0])]; #size = 1; + get size(): number { + return this.#size; + } + add(value: Buffer): number { if (value.length === 0) return 0; - if (value.length > 0x7f) - throw new RangeError("Managed PE fixture blob is too large"); const index = this.#size; - this.#chunks.push(Buffer.from([value.length]), value); - this.#size += value.length + 1; + const prefix = + value.length <= 0x7f + ? Buffer.from([value.length]) + : value.length <= 0x3fff + ? Buffer.from([0x80 | (value.length >> 8), value.length & 0xff]) + : Buffer.from([ + 0xc0 | (value.length >> 24), + (value.length >> 16) & 0xff, + (value.length >> 8) & 0xff, + value.length & 0xff, + ]); + this.#chunks.push(prefix, value); + this.#size += value.length + prefix.length; return index; } @@ -106,33 +132,92 @@ const u32 = (value: number): Buffer => { return bytes; }; -const moduleRow = (name: number): Buffer => - Buffer.concat([u16(0), u16(name), u16(1), u16(0), u16(0)]); +type MetadataIndexSize = 2 | 4; -const typeRefRow = (name: number, namespace: number): Buffer => - Buffer.concat([u16(0), u16(name), u16(namespace)]); +const indexSize = (rows: number, codedTagBits = 0): MetadataIndexSize => + rows < 2 ** (16 - codedTagBits) ? 2 : 4; -const typeDefRow = ( +const metadataIndex = (value: number, size: MetadataIndexSize): Buffer => + size === 2 ? u16(value) : u32(value); + +const codedIndexSize = ( + rows: readonly number[], + tagBits: number, +): MetadataIndexSize => indexSize(Math.max(...rows), tagBits); + +const moduleRow = ( name: number, - namespace: number, - fieldList: number, - methodList: number, + stringSize: MetadataIndexSize, + guidSize: MetadataIndexSize, ): Buffer => Buffer.concat([ - u32(0x0010_0001), - u16(name), - u16(namespace), - u16((1 << 2) | 1), - u16(fieldList), - u16(methodList), + u16(0), + metadataIndex(name, stringSize), + metadataIndex(1, guidSize), + metadataIndex(0, guidSize), + metadataIndex(0, guidSize), ]); -const fieldRow = (name: number, signature: number): Buffer => - Buffer.concat([u16(0x0001), u16(name), u16(signature)]); +const typeRefRow = ( + name: number, + namespace: number, + stringSize: MetadataIndexSize, + resolutionScopeSize: MetadataIndexSize, +): Buffer => + Buffer.concat([ + metadataIndex(0, resolutionScopeSize), + metadataIndex(name, stringSize), + metadataIndex(namespace, stringSize), + ]); + +const typeDefRow = ({ + name, + namespace, + fieldList, + methodList, + stringSize, + typeDefOrRefSize, + fieldListSize, + methodListSize, + extendsRaw = (1 << 2) | 1, +}: { + readonly name: number; + readonly namespace: number; + readonly fieldList: number; + readonly methodList: number; + readonly stringSize: MetadataIndexSize; + readonly typeDefOrRefSize: MetadataIndexSize; + readonly fieldListSize: MetadataIndexSize; + readonly methodListSize: MetadataIndexSize; + readonly extendsRaw?: number; +}): Buffer => + Buffer.concat([ + u32(0x0010_0001), + metadataIndex(name, stringSize), + metadataIndex(namespace, stringSize), + metadataIndex(extendsRaw, typeDefOrRefSize), + metadataIndex(fieldList, fieldListSize), + metadataIndex(methodList, methodListSize), + ]); + +const fieldRow = ( + name: number, + signature: number, + stringSize: MetadataIndexSize, + blobSize: MetadataIndexSize, +): Buffer => + Buffer.concat([ + u16(0x0001), + metadataIndex(name, stringSize), + metadataIndex(signature, blobSize), + ]); interface MethodDefRowInput { readonly name: number; readonly signature: number; + readonly stringSize: MetadataIndexSize; + readonly blobSize: MetadataIndexSize; + readonly parameterSize: MetadataIndexSize; readonly rva?: number; readonly flags?: number; readonly implFlags?: number; @@ -141,6 +226,9 @@ interface MethodDefRowInput { const methodDefRow = ({ name, signature, + stringSize, + blobSize, + parameterSize, rva = 0, flags = 0x0016, implFlags = 0, @@ -149,29 +237,56 @@ const methodDefRow = ({ u32(rva), u16(implFlags), u16(flags), - u16(name), - u16(signature), - u16(0), + metadataIndex(name, stringSize), + metadataIndex(signature, blobSize), + metadataIndex(0, parameterSize), ]); -const memberRefRow = (name: number, signature: number): Buffer => - Buffer.concat([u16((1 << 3) | 1), u16(name), u16(signature)]); +const memberRefRow = ({ + name, + signature, + stringSize, + blobSize, + parentSize, + parentRaw = (1 << 3) | 1, +}: { + readonly name: number; + readonly signature: number; + readonly stringSize: MetadataIndexSize; + readonly blobSize: MetadataIndexSize; + readonly parentSize: MetadataIndexSize; + readonly parentRaw?: number; +}): Buffer => + Buffer.concat([ + metadataIndex(parentRaw, parentSize), + metadataIndex(name, stringSize), + metadataIndex(signature, blobSize), + ]); -const moduleRefRow = (name: number): Buffer => Buffer.concat([u16(name)]); +const moduleRefRow = (name: number, stringSize: MetadataIndexSize): Buffer => + Buffer.concat([metadataIndex(name, stringSize)]); const implMapRow = ( importName: number, importScope: number, mappingFlags: number, + stringSize: MetadataIndexSize, + memberForwardedSize: MetadataIndexSize, + moduleRefSize: MetadataIndexSize, + memberForwardedRaw: number, ): Buffer => Buffer.concat([ u16(mappingFlags), - u16((1 << 1) | 1), - u16(importName), - u16(importScope), + metadataIndex(memberForwardedRaw, memberForwardedSize), + metadataIndex(importName, stringSize), + metadataIndex(importScope, moduleRefSize), ]); -const assemblyRow = (name: number): Buffer => +const assemblyRow = ( + name: number, + stringSize: MetadataIndexSize, + blobSize: MetadataIndexSize, +): Buffer => Buffer.concat([ u32(0x0000_8004), u16(1), @@ -179,29 +294,71 @@ const assemblyRow = (name: number): Buffer => u16(3), u16(4), u32(0), - u16(0), - u16(name), - u16(0), + metadataIndex(0, blobSize), + metadataIndex(name, stringSize), + metadataIndex(0, stringSize), ]); -const assemblyRefRow = (name: number, keyOrToken: number): Buffer => +const assemblyRefRow = ( + name: number, + keyOrToken: number, + malformedName: boolean, + stringSize: MetadataIndexSize, + blobSize: MetadataIndexSize, +): Buffer => Buffer.concat([ u16(8), u16(0), u16(0), u16(0), u32(0), - u16(keyOrToken), - u16(name), - u16(0), - u16(0), + metadataIndex(keyOrToken, blobSize), + metadataIndex( + malformedName ? (stringSize === 2 ? 0xffff : 0xffff_ffff) : name, + stringSize, + ), + metadataIndex(0, stringSize), + metadataIndex(0, blobSize), ]); -const customAttributeRow = (value: number): Buffer => - Buffer.concat([u16((1 << 5) | 14), u16((1 << 3) | 3), u16(value)]); +const customAttributeRow = ( + value: number, + options: ManagedPeFixtureOptions, + parentIndexSize: 2 | 4, + typeIndexSize: MetadataIndexSize, + blobSize: MetadataIndexSize, +): Buffer => + Buffer.concat([ + parentIndexSize === 2 + ? u16( + options.malformedCustomAttributeRows?.includes(1) + ? 0xffff + : (1 << 5) | 14, + ) + : u32( + options.malformedCustomAttributeRows?.includes(1) + ? 0xffff_ffff + : (1 << 5) | 14, + ), + metadataIndex( + options.customAttributeTypeRaw ?? (1 << 3) | 3, + typeIndexSize, + ), + metadataIndex(value, blobSize), + ]); -const manifestResourceRow = (name: number): Buffer => - Buffer.concat([u32(0), u32(2), u16(name), u16(0)]); +const manifestResourceRow = ( + name: number, + stringSize: MetadataIndexSize, + implementationSize: MetadataIndexSize, + implementationRaw: number, +): Buffer => + Buffer.concat([ + u32(0), + u32(2), + metadataIndex(name, stringSize), + metadataIndex(implementationRaw, implementationSize), + ]); const fixedStringAttributeBlob = (value: string): Buffer => { const bytes = Buffer.from(textEncoder.encode(value)); @@ -270,11 +427,13 @@ const metadataRoot = ( const tablesStream = ( rows: ReadonlyMap, extraValidMask: bigint, + heapSizes: number, ): Buffer => { let valid = extraValidMask; for (const table of rows.keys()) valid |= 1n << BigInt(table); const header = Buffer.alloc(24); header.writeUInt8(2, 4); + header.writeUInt8(heapSizes, 6); header.writeUInt8(1, 7); header.writeBigUInt64LE(valid, 8); const counts: Buffer[] = []; @@ -292,8 +451,269 @@ const tablesStream = ( return Buffer.concat([header, ...counts, ...data]); }; +interface FixtureMetadataRowContext { + readonly options: ManagedPeFixtureOptions; + readonly methodRva: number; + readonly referenceStringIndexes: readonly number[]; + readonly indexes: { + readonly moduleName: number; + readonly assemblyName: number; + readonly attributeName: number; + readonly attributeNamespace: number; + readonly constructorName: number; + readonly typeName: number; + readonly typeNamespace: number; + readonly fieldName: number; + readonly methodName: number; + readonly pinvokeModuleName: number | null; + readonly pinvokeImportName: number | null; + readonly resourceName: number; + readonly tokenBlob: number; + readonly fieldSignature: number; + readonly methodSignature: number; + readonly constructorSignature: number; + readonly attributeBlob: number; + }; + readonly heapSizes: { + readonly strings: MetadataIndexSize; + readonly blobs: MetadataIndexSize; + readonly guids: MetadataIndexSize; + }; +} + +interface FixtureIndexWidths { + readonly table: (index: number) => MetadataIndexSize; + readonly coded: ( + tables: readonly number[], + tagBits: number, + ) => MetadataIndexSize; + readonly resolutionScope: MetadataIndexSize; + readonly typeDefOrRef: MetadataIndexSize; + readonly memberRefParent: MetadataIndexSize; + readonly hasCustomAttribute: MetadataIndexSize; + readonly customAttributeType: MetadataIndexSize; + readonly implementation: MetadataIndexSize; +} + +const repeatedRows = ( + count: number, + createRow: () => Buffer, +): readonly Buffer[] => Array.from({ length: count }, createRow); + +const customAttributeRows = ( + { + options, + indexes, + heapSizes, + }: Pick, + widths: FixtureIndexWidths, +): readonly Buffer[] => + options.targetFramework === null + ? [] + : [ + customAttributeRow( + indexes.attributeBlob, + options, + widths.hasCustomAttribute, + widths.customAttributeType, + heapSizes.blobs, + ), + ]; + +const fixtureIndexWidths = ({ + options, + referenceStringIndexes, + indexes, +}: FixtureMetadataRowContext): FixtureIndexWidths => { + const rowCounts = new Map([ + [0, options.moduleRowCount ?? 1], + [1, 1], + [2, 1], + [4, 1], + [6, 1], + [8, 0], + [10, 1], + [12, options.targetFramework === null ? 0 : 1], + ...(indexes.pinvokeModuleName === null ? [] : ([[26, 1]] as const)), + ...(indexes.pinvokeImportName === null ? [] : ([[28, 1]] as const)), + [32, options.assemblyRowCount ?? 1], + [35, referenceStringIndexes.length], + [40, 1], + ]); + const count = (table: number): number => rowCounts.get(table) ?? 0; + const table = (index: number): MetadataIndexSize => indexSize(count(index)); + const coded = ( + tables: readonly number[], + tagBits: number, + ): MetadataIndexSize => codedIndexSize(tables.map(count), tagBits); + return { + table, + coded, + resolutionScope: coded([0, 26, 35, 1], 2), + typeDefOrRef: coded([2, 1, 27], 2), + memberRefParent: coded([2, 1, 26, 6, 27], 3), + hasCustomAttribute: coded( + [ + 6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42, + 44, 43, + ], + 5, + ), + customAttributeType: coded([6, 10], 3), + implementation: coded([38, 35, 39], 2), + }; +}; + +const fixtureMetadataRows = ({ + options, + methodRva, + referenceStringIndexes, + indexes, + heapSizes, +}: FixtureMetadataRowContext): ReadonlyMap => { + const widths = fixtureIndexWidths({ + options, + methodRva, + referenceStringIndexes, + indexes, + heapSizes, + }); + const rows = new Map([ + [ + 0, + repeatedRows(options.moduleRowCount ?? 1, () => + moduleRow(indexes.moduleName, heapSizes.strings, heapSizes.guids), + ), + ], + [ + 1, + [ + typeRefRow( + indexes.attributeName, + indexes.attributeNamespace, + heapSizes.strings, + widths.resolutionScope, + ), + ], + ], + [ + 2, + [ + typeDefRow({ + name: indexes.typeName, + namespace: indexes.typeNamespace, + fieldList: 1, + methodList: 1, + stringSize: heapSizes.strings, + typeDefOrRefSize: widths.typeDefOrRef, + fieldListSize: widths.table(4), + methodListSize: widths.table(6), + ...(options.extendsRaw === undefined + ? {} + : { extendsRaw: options.extendsRaw }), + }), + ], + ], + [ + 4, + [ + fieldRow( + indexes.fieldName, + indexes.fieldSignature, + heapSizes.strings, + heapSizes.blobs, + ), + ], + ], + [ + 6, + [ + methodDefRow({ + name: indexes.methodName, + signature: indexes.methodSignature, + stringSize: heapSizes.strings, + blobSize: heapSizes.blobs, + parameterSize: widths.table(8), + rva: methodRva, + flags: options.pinvoke === undefined ? 0x0016 : 0x2016, + }), + ], + ], + [ + 10, + [ + memberRefRow({ + name: indexes.constructorName, + signature: indexes.constructorSignature, + stringSize: heapSizes.strings, + blobSize: heapSizes.blobs, + parentSize: widths.memberRefParent, + ...(options.memberRefParentRaw === undefined + ? {} + : { parentRaw: options.memberRefParentRaw }), + }), + ], + ], + [12, customAttributeRows({ options, indexes, heapSizes }, widths)], + ...(indexes.pinvokeModuleName === null + ? [] + : ([ + [26, [moduleRefRow(indexes.pinvokeModuleName, heapSizes.strings)]], + ] as const)), + ...(indexes.pinvokeImportName === null + ? [] + : ([ + [ + 28, + [ + implMapRow( + indexes.pinvokeImportName, + 1, + options.pinvoke?.mappingFlags ?? 0x0344, + heapSizes.strings, + widths.coded([4, 6], 1), + widths.table(26), + options.pinvoke?.memberForwardedRaw ?? (1 << 1) | 1, + ), + ], + ], + ] as const)), + [ + 32, + repeatedRows(options.assemblyRowCount ?? 1, () => + assemblyRow(indexes.assemblyName, heapSizes.strings, heapSizes.blobs), + ), + ], + [ + 35, + referenceStringIndexes.map((name, index) => + assemblyRefRow( + name, + indexes.tokenBlob, + options.malformedAssemblyReferenceRows?.includes(index + 1) ?? false, + heapSizes.strings, + heapSizes.blobs, + ), + ), + ], + [ + 40, + [ + manifestResourceRow( + indexes.resourceName, + heapSizes.strings, + widths.implementation, + options.resourceImplementationRaw ?? 0, + ), + ], + ], + ]); + return rows; +}; + const buildManagedFixtureMetadata = ( options: ManagedPeFixtureOptions, + methodRva: number, ): Buffer => { const strings = new StringHeap(); const blobs = new BlobHeap(); @@ -329,50 +749,45 @@ const buildManagedFixtureMetadata = ( const constructorSignature = blobs.add(Buffer.from([0x20, 0x01, 0x01, 0x0e])); const attributeBlob = blobs.add( fixedStringAttributeBlob( - options.targetFramework ?? ".NETCoreApp,Version=v8.0", + options.targetFramework === null + ? ".NETCoreApp,Version=v8.0" + : (options.targetFramework ?? ".NETCoreApp,Version=v8.0"), ), ); - const rows = new Map([ - [0, [moduleRow(moduleName)]], - [1, [typeRefRow(attributeName, attributeNamespace)]], - [2, [typeDefRow(typeName, typeNamespace, 1, 1)]], - [4, [fieldRow(fieldName, fieldSignature)]], - [ - 6, - [ - methodDefRow({ - name: methodName, - signature: methodSignature, - rva: 0x2800, - flags: options.pinvoke === undefined ? 0x0016 : 0x2016, - }), - ], - ], - [10, [memberRefRow(constructorName, constructorSignature)]], - [12, [customAttributeRow(attributeBlob)]], - ...(pinvokeModuleName === null - ? [] - : ([[26, [moduleRefRow(pinvokeModuleName)]]] as const)), - ...(pinvokeImportName === null - ? [] - : ([ - [ - 28, - [ - implMapRow( - pinvokeImportName, - 1, - options.pinvoke?.mappingFlags ?? 0x0344, - ), - ], - ], - ] as const)), - [32, [assemblyRow(assemblyName)]], - [35, referenceStringIndexes.map((name) => assemblyRefRow(name, tokenBlob))], - [40, [manifestResourceRow(resourceName)]], - ]); + const stringSize = indexSize(strings.size); + const blobSize = indexSize(blobs.size); + const guidSize = indexSize(guidHeap(options.mvid).length); + const rows = fixtureMetadataRows({ + options, + methodRva, + referenceStringIndexes, + indexes: { + moduleName, + assemblyName, + attributeName, + attributeNamespace, + constructorName, + typeName, + typeNamespace, + fieldName, + methodName, + pinvokeModuleName, + pinvokeImportName, + resourceName, + tokenBlob, + fieldSignature, + methodSignature, + constructorSignature, + attributeBlob, + }, + heapSizes: { strings: stringSize, blobs: blobSize, guids: guidSize }, + }); + const heapFlags = + (stringSize === 4 ? 0x01 : 0) | + (guidSize === 4 ? 0x02 : 0) | + (blobSize === 4 ? 0x04 : 0); const metadata = metadataRoot( - tablesStream(rows, options.metadataValidMaskExtra ?? 0n), + tablesStream(rows, options.metadataValidMaskExtra ?? 0n, heapFlags), strings.toBuffer(), guidHeap(options.mvid), blobs.toBuffer(), @@ -383,14 +798,56 @@ const buildManagedFixtureMetadata = ( const buildManagedFixtureImage = ( options: ManagedPeFixtureOptions, - metadata: Buffer, + buildMetadata: (methodRva: number) => Buffer, ): Buffer => { const resourceData = options.resourceData ?? Buffer.from("resource-data"); const resourceDirectory = Buffer.concat([ u32(resourceData.length), resourceData, ]); - const image = Buffer.alloc(0x1000); + const initialMetadata = buildMetadata(0); + const resourceOffset = Math.max( + 0x0800, + Math.ceil((0x0300 + initialMetadata.length) / 0x0200) * 0x0200, + ); + const resourceRva = 0x2000 + resourceOffset - 0x0200; + const bodyOffset = Math.max( + 0x0a00, + Math.ceil((resourceOffset + resourceDirectory.length) / 0x0200) * 0x0200, + ); + const methodRva = 0x2000 + bodyOffset - 0x0200; + const metadata = buildMetadata(methodRva); + const body = + options.ilBody ?? + Buffer.from([ + 0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a, + 0x2a, + ]); + const legacyReadyToRunOffset = 0x0900; + const metadataEnd = 0x0300 + metadata.length; + const resourceEnd = resourceOffset + resourceDirectory.length; + const bodyEnd = bodyOffset + body.length; + const overlapsReadyToRun = (start: number, end: number): boolean => + legacyReadyToRunOffset < end && legacyReadyToRunOffset + 4 > start; + const readyToRunOffset = + options.readyToRun === true + ? overlapsReadyToRun(0x0300, metadataEnd) || + overlapsReadyToRun(resourceOffset, resourceEnd) || + overlapsReadyToRun(bodyOffset, bodyEnd) + ? Math.ceil(Math.max(metadataEnd, resourceEnd, bodyEnd) / 4) * 4 + : legacyReadyToRunOffset + : null; + const rawSectionSize = + Math.ceil( + Math.max( + 0x0e00, + 0x0100 + metadata.length, + bodyOffset - 0x0200 + body.length, + readyToRunOffset === null ? 0 : readyToRunOffset - 0x0200 + 4, + ) / 0x0200, + ) * 0x0200; + const virtualSectionSize = Math.max(0x1000, rawSectionSize); + const image = Buffer.alloc(0x0200 + rawSectionSize); image.write("MZ", 0, "ascii"); image.writeUInt32LE(0x80, 0x3c); image.writeUInt32LE(0x0000_4550, 0x80); @@ -405,16 +862,19 @@ const buildManagedFixtureImage = ( image.writeUInt32LE(0x0040_0000, optional + 28); image.writeUInt32LE(0x1000, optional + 32); image.writeUInt32LE(0x200, optional + 36); - image.writeUInt32LE(0x3000, optional + 56); + image.writeUInt32LE( + Math.ceil((0x2000 + virtualSectionSize) / 0x1000) * 0x1000, + optional + 56, + ); image.writeUInt32LE(0x200, optional + 60); image.writeUInt32LE(16, optional + 92); image.writeUInt32LE(0x2000, optional + 96 + 14 * 8); image.writeUInt32LE(72, optional + 96 + 14 * 8 + 4); const section = optional + 0x00e0; image.write(".text\0\0\0", section, "ascii"); - image.writeUInt32LE(0x1000, section + 8); + image.writeUInt32LE(virtualSectionSize, section + 8); image.writeUInt32LE(0x2000, section + 12); - image.writeUInt32LE(0x0e00, section + 16); + image.writeUInt32LE(rawSectionSize, section + 16); image.writeUInt32LE(0x0200, section + 20); image.writeUInt32LE(0x6000_0020, section + 36); const cli = 0x0200; @@ -425,22 +885,16 @@ const buildManagedFixtureImage = ( image.writeUInt32LE(metadata.length, cli + 12); image.writeUInt32LE(options.cliFlags ?? 1, cli + 16); image.writeUInt32LE(0x0600_0001, cli + 20); - image.writeUInt32LE(0x2600, cli + 24); + image.writeUInt32LE(resourceRva, cli + 24); image.writeUInt32LE(resourceDirectory.length, cli + 28); - if (options.readyToRun === true) { - image.writeUInt32LE(0x2700, cli + 64); + if (readyToRunOffset !== null) { + image.writeUInt32LE(0x2000 + readyToRunOffset - 0x0200, cli + 64); image.writeUInt32LE(4, cli + 68); - image.write("RTR\0", 0x0900, "ascii"); + image.write("RTR\0", readyToRunOffset, "ascii"); } - ( - options.ilBody ?? - Buffer.from([ - 0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a, - 0x2a, - ]) - ).copy(image, 0x0a00); + body.copy(image, bodyOffset); metadata.copy(image, 0x0300); - resourceDirectory.copy(image, 0x0800); + resourceDirectory.copy(image, resourceOffset); return image; }; @@ -448,7 +902,9 @@ const buildManagedFixtureImage = ( export const buildManagedPeFixture = ( options: ManagedPeFixtureOptions = {}, ): Buffer => - buildManagedFixtureImage(options, buildManagedFixtureMetadata(options)); + buildManagedFixtureImage(options, (methodRva) => + buildManagedFixtureMetadata(options, methodRva), + ); /** Build a syntactically valid PE32 fixture with no CLI directory. */ export const buildNativePeFixture = (): Buffer => { diff --git a/src/dotnet/ManagedPeReader.ts b/src/dotnet/ManagedPeReader.ts index 2f2f7fdba0..c888a2b4bc 100644 --- a/src/dotnet/ManagedPeReader.ts +++ b/src/dotnet/ManagedPeReader.ts @@ -41,6 +41,7 @@ export interface ManagedPeLayout { readonly cliDirectoryPresent: boolean; readonly cliIssue: ManagedParseIssue | null; rvaToOffset(rva: number, size: number, scope: string): number; + rvaAvailableBytes(rva: number, scope: string): number; } interface RvaMapping { @@ -355,5 +356,22 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => { cliIssue, rvaToOffset: (rva, size, scope) => mapRva({ bytes, sections, rva, size, scope }), + rvaAvailableBytes: (rva, scope) => { + mapRva({ bytes, sections, rva, size: 1, scope }); + for (const section of sections) { + if (rva < section.virtualAddress) continue; + const within = rva - section.virtualAddress; + if (within < section.rawSize) + return Math.min( + section.rawSize - within, + bytes.length - section.rawOffset - within, + ); + } + throw managedFailure( + "invalid-directory", + scope, + `${scope} RVA is not covered by file-backed PE section data`, + ); + }, }; }; diff --git a/src/evaluation/CodexAgentEval.ts b/src/evaluation/CodexAgentEval.ts index e5fc0c7d7c..e193761b32 100644 --- a/src/evaluation/CodexAgentEval.ts +++ b/src/evaluation/CodexAgentEval.ts @@ -1,3 +1,5 @@ +import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; + /** One REA MCP invocation observed in a Codex JSONL transcript. */ interface CodexMcpCall { readonly id: string | null; @@ -47,7 +49,7 @@ const canonicalValue = (value: unknown): unknown => { if (object === undefined) return value; return Object.fromEntries( Object.entries(object) - .sort(([left], [right]) => left.localeCompare(right)) + .sort(([left], [right]) => compareUnicodeCodePoints(left, right)) .map(([key, child]) => [key, canonicalValue(child)]), ); }; diff --git a/src/evm/EvmBytecodeCarrier.test.ts b/src/evm/EvmBytecodeCarrier.test.ts new file mode 100644 index 0000000000..b8d485f993 --- /dev/null +++ b/src/evm/EvmBytecodeCarrier.test.ts @@ -0,0 +1,34 @@ +import { expect, it } from "vitest"; +import { + decodeEvmBytecodeCarrier, + EvmCarrierFailure, +} from "./EvmBytecodeCarrier.js"; + +it.each(["6000FF", "0x6000ff", " \t0X6000FF\r\n"])( + "decodes explicit hex without changing selected bytes: %j", + (text) => { + expect( + Buffer.from(decodeEvmBytecodeCarrier(Buffer.from(text), "hex")).toString( + "hex", + ), + ).toBe("6000ff"); + }, +); +it.each(["0x1", "60 00", "0xzz", "\uFEFF6000", "\u00a06000"])( + "rejects malformed carrier rather than guessing bytecode: %j", + (text) => { + expect(() => decodeEvmBytecodeCarrier(Buffer.from(text), "hex")).toThrow( + EvmCarrierFailure, + ); + }, +); +it("distinguishes raw bytes, empty code and malformed UTF-8", () => { + const bytes = Uint8Array.from([0xff, 0x00]); + const decoded = decodeEvmBytecodeCarrier(bytes, "raw"); + expect(decoded).toEqual(bytes); + expect(decoded).not.toBe(bytes); + expect(() => decodeEvmBytecodeCarrier(bytes, "hex")).toThrow("valid UTF-8"); + expect(decodeEvmBytecodeCarrier(Buffer.from("0x"), "hex")).toEqual( + new Uint8Array(), + ); +}); diff --git a/src/evm/EvmBytecodeCarrier.ts b/src/evm/EvmBytecodeCarrier.ts new file mode 100644 index 0000000000..368b674aff --- /dev/null +++ b/src/evm/EvmBytecodeCarrier.ts @@ -0,0 +1,26 @@ +/** Expected carrier encoding failure, independent of the analysis engine. */ +export class EvmCarrierFailure extends Error {} + +/** Decode only the explicitly selected representation; source carrier bytes are retained separately. */ +export const decodeEvmBytecodeCarrier = ( + bytes: Uint8Array, + encoding: "raw" | "hex", +): Uint8Array => { + if (encoding === "raw") return Uint8Array.from(bytes); + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode( + bytes, + ); + } catch (cause: unknown) { + throw new EvmCarrierFailure("Hex carrier must be valid UTF-8.", { cause }); + } + const code = text + .replace(/^[\x09-\x0d\x20]+|[\x09-\x0d\x20]+$/g, "") + .replace(/^0x/i, ""); + if (!/^(?:[a-fA-F0-9]{2})*$/.test(code)) + throw new EvmCarrierFailure( + "Hex carrier requires complete hexadecimal byte pairs, an optional 0x prefix and outer ASCII whitespace; interior separators are invalid.", + ); + return Uint8Array.from(Buffer.from(code, "hex")); +}; diff --git a/src/evm/EvmWorkerLimits.test.ts b/src/evm/EvmWorkerLimits.test.ts new file mode 100644 index 0000000000..fcbf8197a8 --- /dev/null +++ b/src/evm/EvmWorkerLimits.test.ts @@ -0,0 +1,55 @@ +import { expect, it } from "vitest"; +import { selectEvmWorkerLimits } from "./EvmWorkerLimits.js"; + +const limits = ( + soft = "unlimited", + hard = "unlimited", +) => `Limit Soft Limit Hard Limit Units +Max address space ${soft} ${hard} bytes +Max cpu time unlimited unlimited seconds +Max file size unlimited unlimited bytes +`; +it("uses configured maxima without equating virtual space and RSS", () => { + expect(selectEvmWorkerLimits(limits())).toEqual({ + ok: true, + value: { + addressSpaceBytes: 3221225472, + cpuSeconds: 30, + fileSizeBytes: 16777216, + }, + }); +}); +it.each([ + ["1073741824", "2147483648", 1073741824], + ["2147483648", "2147483648", 2147483648], + ["0", "0", 0], + ["9223372036854775807", "9223372036854775807", 3221225472], +])( + "preserves tighter soft/hard boundaries without integer loss: %s/%s", + (soft, hard, expected) => { + expect(selectEvmWorkerLimits(limits(soft, hard))).toMatchObject({ + ok: true, + value: { addressSpaceBytes: expected }, + }); + }, +); +it("independently preserves CPU and file output restrictions", () => { + expect( + selectEvmWorkerLimits( + limits() + .replace("time unlimited unlimited", "time 5 10") + .replace("size unlimited unlimited", "size 4096 8192"), + ), + ).toMatchObject({ ok: true, value: { cpuSeconds: 5, fileSizeBytes: 4096 } }); +}); +it.each([ + ["not-text", null], + ["soft-unlimited-hard-finite", limits("unlimited", "2147483648")], + ["soft-exceeds-hard", limits("3", "2")], + ["missing-row", limits().replace("Max file size", "Max other size")], + ["duplicate-row", limits() + "Max cpu time 1 1 seconds\n"], + ["wrong-unit", limits().replace("bytes", "pages")], + ["oversized", "x".repeat(65537)], +])("rejects malformed/ambiguous resource observations: %s", (_label, input) => { + expect(selectEvmWorkerLimits(input).ok).toBe(false); +}); diff --git a/src/evm/EvmWorkerLimits.ts b/src/evm/EvmWorkerLimits.ts new file mode 100644 index 0000000000..069152e2d3 --- /dev/null +++ b/src/evm/EvmWorkerLimits.ts @@ -0,0 +1,58 @@ +import { err, ok, type Result } from "../domain/result.js"; +import { EVM_INTERFACE_LIMITS } from "./EvmoleRelease.js"; + +/** Selected worker soft limits to configure; inherited hard limits are left unchanged. */ +export interface EvmWorkerLimits { + readonly addressSpaceBytes: number; + readonly cpuSeconds: number; + readonly fileSizeBytes: number; +} + +/** Read only the three selected Linux limits, retaining tighter soft/hard boundaries. */ +export const selectEvmWorkerLimits = ( + raw: unknown, +): Result => { + if (typeof raw !== "string" || Buffer.byteLength(raw) > 64 * 1024) + return err( + "Linux resource limits must be text within the 64 KiB parsing budget.", + ); + const selected: number[] = []; + for (const [name, units, maximum] of [ + ["Max address space", "bytes", EVM_INTERFACE_LIMITS.addressSpaceBytes], + ["Max cpu time", "seconds", 30], + ["Max file size", "bytes", EVM_INTERFACE_LIMITS.outputBytes], + ] as const) { + const entries = raw + .split("\n") + .filter((line) => line.startsWith(name + " ")); + if (entries.length !== 1) + return err(`Linux resource limits must report exactly one ${name} row.`); + const match = new RegExp( + `^${name}\\s+(unlimited|[0-9]+)\\s+(unlimited|[0-9]+)\\s+${units}\\s*$`, + ).exec(entries[0] ?? ""); + if (match === null) + return err(`Malformed inherited Linux resource limit: ${name}.`); + let effective = BigInt(maximum); + const soft = match[1]; + const hard = match[2]; + if (soft === undefined || hard === undefined) + return err(`Missing ${name} values.`); + if ( + hard !== "unlimited" && + (soft === "unlimited" || BigInt(soft) > BigInt(hard)) + ) + return err(`Inherited ${name} soft limit exceeds its hard limit.`); + for (const value of [soft, hard]) + if (value !== "unlimited" && BigInt(value) < effective) + effective = BigInt(value); + selected.push(Number(effective)); + } + const [addressSpaceBytes, cpuSeconds, fileSizeBytes] = selected; + if ( + addressSpaceBytes === undefined || + cpuSeconds === undefined || + fileSizeBytes === undefined + ) + return err("Required Linux resource limits were not selected."); + return ok({ addressSpaceBytes, cpuSeconds, fileSizeBytes }); +}; diff --git a/src/evm/EvmoleFailures.ts b/src/evm/EvmoleFailures.ts new file mode 100644 index 0000000000..5690a079ab --- /dev/null +++ b/src/evm/EvmoleFailures.ts @@ -0,0 +1,205 @@ +import type { EvmWorkerLimits } from "./EvmWorkerLimits.js"; +import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; +import { + AnalysisError, + type AnalysisCapturedOutput, +} from "../domain/analysisErrorBase.js"; +import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js"; +import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisCapabilityUnavailableError, + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, + AnalysisResourceConstraintError, + AnalysisTimeoutError, +} from "../domain/analysisErrorCore.js"; +import { ProviderAdapterError } from "../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../domain/providerCleanupError.js"; +import { OwnedCommandFailure } from "../process/OwnedCommand.js"; +import { + EVMOLE_PROVIDER_IDENTITY, + EVM_INTERFACE_LIMITS, + EVM_FILE_SIZE_FAILURE_EXIT, +} from "./EvmoleRelease.js"; + +/** Correlate a reserved exit with the owned worker's actual failure branch. */ +export interface EvmFileSizeFailureEvidence { + readonly verified: boolean; + readonly failure: string | null; +} + +/** Keep carrier read, engine and owned lifecycle failures distinct. */ +export const evmInterfaceFailure = ( + cause: unknown, + phase: "configuration" | "artifact-read" | "worker", + path: string, + workerLimits?: EvmWorkerLimits, + limiter = "/usr/bin/prlimit", + fileFailure?: EvmFileSizeFailureEvidence, +): AnalysisError => { + if (cause instanceof AnalysisError) return cause; + if (cause instanceof OwnedCommandFailure) { + const outputOptions = + cause.snapshot === null + ? undefined + : { capturedOutput: capturedEvmOutput(cause.snapshot) }; + if (cause.cleanupFailure !== null) + return new ProviderCleanupError( + EVMOLE_PROVIDER_IDENTITY.id, + cause.resources, + { + reason: cause.cleanupFailure, + ...(cause.snapshot === null + ? {} + : { captured_output: { ...capturedEvmOutput(cause.snapshot) } }), + previous_error: { + failure_kind: cause.reason, + message: cause.message, + exit_code: cause.snapshot?.exitCode ?? null, + signal: cause.snapshot?.signal ?? null, + stdout: cause.snapshot?.stdout.text ?? null, + stderr: cause.snapshot?.stderr.text ?? null, + }, + }, + { operation: "inspect_evm_interface", cause }, + ); + if (cause.reason === "cancelled") + return new AnalysisCancelledError("inspect_evm_interface", outputOptions); + if (cause.reason === "timeout") + return new AnalysisTimeoutError( + "inspect_evm_interface", + EVM_INTERFACE_LIMITS.timeoutMs, + outputOptions, + ); + if (cause.reason === "output-limit") + return new AnalysisOutputError( + "inspect_evm_interface", + cause.message, + outputOptions, + ); + if ( + cause.reason === "process" && + (cause.snapshot?.signal === "SIGXCPU" || + cause.snapshot?.signal === "SIGXFSZ" || + (fileFailure?.verified === true && + cause.snapshot?.exitCode === EVM_FILE_SIZE_FAILURE_EXIT && + cause.snapshot.signal === null)) + ) + return new AnalysisResourceConstraintError( + "inspect_evm_interface", + cause.snapshot.signal === "SIGXCPU" ? "cpu" : "file-size", + (cause.snapshot.signal === null + ? "Owned EVM worker reported EFBIG while writing its reply; the exact write failure cause is unknown." + : `Owned EVM command terminated with ${cause.snapshot.signal}; the exact signal cause is unknown.`) + + " Effective worker limits are unknown. Configured soft limits are retained separately.", + workerLimits === undefined + ? null + : { + configured_soft_limits: { + address_space_bytes: workerLimits.addressSpaceBytes, + cpu_seconds: workerLimits.cpuSeconds, + file_size_bytes: workerLimits.fileSizeBytes, + }, + effective_soft_limits: null, + }, + outputOptions, + ); + } + if ( + cause instanceof Error && + "code" in cause && + "syscall" in cause && + typeof cause.syscall === "string" && + cause.syscall.startsWith("spawn ") + ) { + const reason = `Configured util-linux prlimit could not launch (${String(cause.code)}): ${limiter}. Check its executable/interpreter and host execute access.`; + return new AnalysisCapabilityUnavailableError( + EVMOLE_PROVIDER_IDENTITY.id, + "inspect_evm_interface", + reason, + { cause, userMessage: reason }, + ); + } + if (cause instanceof ArtifactReaderFailure) { + if (cause.reason === "cancelled") + return new AnalysisCancelledError("inspect_evm_interface"); + if (cause.reason === "integrity") + return new AnalysisArtifactChangedError( + "inspect_evm_interface", + path, + cause.message, + { cause }, + ); + if ( + phase === "artifact-read" && + ["limit", "path", "format"].includes(cause.reason) + ) + return new AnalysisInputError("inspect_evm_interface", { cause }, [ + { + path: ["path"], + reason: cause.reason === "limit" ? "out_of_range" : "invalid_format", + message: cause.message, + }, + ]); + } + if (phase === "artifact-read" && cause instanceof Error && "code" in cause) { + if (cause.code === "EACCES" || cause.code === "EPERM") + return new AnalysisAccessDeniedError( + "inspect_evm_interface", + path, + cause.code, + { cause }, + ); + if (cause.code === "ENOENT" || cause.code === "ENOTDIR") + return new AnalysisInputError("inspect_evm_interface", { cause }, [ + { + path: ["path"], + reason: "invalid_value", + message: `Selected bytecode carrier could not be read (${String(cause.code)}): ${path}.`, + }, + ]); + } + return new ProviderAdapterError( + EVMOLE_PROVIDER_IDENTITY.id, + "inspect_evm_interface", + { + cause, + diagnostics: { + phase, + path, + ...(workerLimits === undefined + ? {} + : { configured_resource_limits: { ...workerLimits } }), + reason: cause instanceof Error ? cause.message : String(cause), + ...(fileFailure === undefined + ? {} + : { file_size_failure_marker: { ...fileFailure } }), + ...(cause instanceof OwnedCommandFailure + ? { + failure_kind: cause.reason, + exit_code: cause.snapshot?.exitCode ?? null, + signal: cause.snapshot?.signal ?? null, + stdout: cause.snapshot?.stdout.text ?? null, + stderr: cause.snapshot?.stderr.text ?? null, + ...(cause.snapshot === null + ? {} + : { + captured_output: { ...capturedEvmOutput(cause.snapshot) }, + }), + } + : {}), + }, + }, + ); +}; + +/** Preserve retained worker diagnostics with their observed truncation status. */ +export const capturedEvmOutput = ( + snapshot: ProviderProcessSnapshot, +): AnalysisCapturedOutput => ({ + stdout: snapshot.stdout.text, + stderr: snapshot.stderr.text, + truncated: snapshot.diagnosticTruncated === true, +}); diff --git a/src/evm/EvmoleInterfaceAdapter.test.ts b/src/evm/EvmoleInterfaceAdapter.test.ts new file mode 100644 index 0000000000..a8b53f2801 --- /dev/null +++ b/src/evm/EvmoleInterfaceAdapter.test.ts @@ -0,0 +1,66 @@ +import { expect, it } from "vitest"; +import { projectEvmoleInterface } from "./EvmoleInterfaceAdapter.js"; + +it("preserves the actual producer keys, duplicates and unknown inference absence", () => { + const raw = { + functions: [ + { selector: "aabbccdd", bytecodeOffset: 1, dispatch: "abi" }, + { + selector: "aabbccdd", + bytecodeOffset: 2, + dispatch: "fallback", + arguments: "uint256", + stateMutability: "view", + }, + ], + }; + const result = projectEvmoleInterface(raw); + expect(result.raw).toEqual(raw); + expect(result.functions).toEqual([ + { + selector: "0xaabbccdd", + bytecode_offset: 1, + dispatch: "abi", + inferred_arguments: null, + inferred_state_mutability: null, + }, + { + selector: "0xaabbccdd", + bytecode_offset: 2, + dispatch: "fallback", + inferred_arguments: "uint256", + inferred_state_mutability: "view", + }, + ]); + expect(projectEvmoleInterface({}).functions).toEqual([]); + const metadata = { + bytecodeOffset: 1, + cborLength: 4, + entries: [ + { key: "reported", value: { type: "integer", value: 9007199254740992 } }, + ], + }; + expect( + projectEvmoleInterface({ functions: [], metadata }).raw.metadata, + ).toEqual(metadata); + expect( + projectEvmoleInterface({ + functions: [], + storage: undefined, + transientStorage: undefined, + disassembled: undefined, + basicBlocks: undefined, + controlFlowGraph: undefined, + metadata: undefined, + }).functions, + ).toEqual([]); +}); +it.each([ + { functions: [{ selector: "bad", bytecodeOffset: 0, dispatch: "abi" }] }, + { + functions: [{ selector: "aabbccdd", bytecodeOffset: -1, dispatch: "abi" }], + }, + { metadata: {} }, +])("rejects producer representations outside the selected profile", (raw) => { + expect(() => projectEvmoleInterface(raw)).toThrow(); +}); diff --git a/src/evm/EvmoleInterfaceAdapter.ts b/src/evm/EvmoleInterfaceAdapter.ts new file mode 100644 index 0000000000..da62d28b9a --- /dev/null +++ b/src/evm/EvmoleInterfaceAdapter.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; + +const offset = z.number().int().nonnegative(); +const reportedMetadata = z.strictObject({ + bytecodeOffset: offset, + cborLength: offset, + entries: z.array( + z.strictObject({ + key: z.string(), + value: z.discriminatedUnion("type", [ + z.strictObject({ type: z.literal("string"), value: z.string() }), + z.strictObject({ type: z.literal("integer"), value: z.number() }), + z.strictObject({ type: z.literal("bytes"), value: z.string() }), + z.strictObject({ type: z.literal("bool"), value: z.boolean() }), + z.strictObject({ type: z.literal("undecoded"), value: z.string() }), + ]), + }), + ), +}); +/** Parse the exact producer representation before assigning portable field names. */ +export const evmoleInterfaceReplySchema = z.strictObject({ + // The unchanged JS binding exposes disabled facets as own undefined keys. + // Their JSON transport representation is absence; actual facet values are + // outside this interface-only profile and must still fail validation. + storage: z.undefined().optional(), + transientStorage: z.undefined().optional(), + disassembled: z.undefined().optional(), + basicBlocks: z.undefined().optional(), + controlFlowGraph: z.undefined().optional(), + // Upstream also extracts metadata when selectors are requested, regardless + // of its metadata flag. Retain its reported values without precision claims. + metadata: reportedMetadata.optional(), + functions: z + .array( + z.strictObject({ + selector: z.string().regex(/^[a-f0-9]{8}$/), + bytecodeOffset: offset, + dispatch: z.enum(["abi", "fallback"]), + arguments: z.string().optional(), + stateMutability: z.string().optional(), + }), + ) + .optional(), +}); + +/** Preserve inference strings and unknown absence; selectors use the conventional 0x display prefix. */ +export const projectEvmoleInterface = (raw: unknown) => { + const producer = evmoleInterfaceReplySchema.parse(raw); + return { + raw: producer, + functions: (producer.functions ?? []).map((fn) => ({ + selector: `0x${fn.selector}`, + bytecode_offset: fn.bytecodeOffset, + dispatch: fn.dispatch, + inferred_arguments: fn.arguments ?? null, + inferred_state_mutability: fn.stateMutability ?? null, + })), + }; +}; diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts new file mode 100644 index 0000000000..2316f44a75 --- /dev/null +++ b/src/evm/EvmoleInterfaceProvider.ts @@ -0,0 +1,401 @@ +import { createHash, randomUUID } from "node:crypto"; +import { access, readFile, stat, writeFile } from "node:fs/promises"; +import { constants } from "node:fs"; +import { isAbsolute, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; +import { + createAnalysisExecution, + type AnalysisExecution, + type ExecutionOptions, +} from "../application/AnalysisProvider.js"; +import type { EvmInterfacePort } from "../application/evm/EvmInterfacePort.js"; +import { readStableArtifact } from "../artifacts/readStableArtifact.js"; +import { OwnedCommandFailure } from "../process/OwnedCommand.js"; +import type { + AnalysisError, + AnalysisCapturedOutput, +} from "../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisUnsupportedTargetError, + AnalysisInputError, + AnalysisOutputError, +} from "../domain/analysisErrorCore.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import type { InspectEvmInterfaceInput } from "../domain/evm/evmInterface.js"; +import { evmInterfaceSchema } from "../domain/evm/evmInterface.js"; +import { ProviderAdapterError } from "../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../domain/providerCleanupError.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { runOwnedCommand } from "../process/OwnedCommand.js"; +import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; +import { + evmInterfaceFailure, + capturedEvmOutput, + type EvmFileSizeFailureEvidence, +} from "./EvmoleFailures.js"; +import { + evmoleInterfaceReplySchema, + projectEvmoleInterface, +} from "./EvmoleInterfaceAdapter.js"; +import { + EVMOLE_PROVIDER_IDENTITY, + EVM_INTERFACE_LIMITS, + EVM_FILE_SIZE_FAILURE_EXIT, +} from "./EvmoleRelease.js"; +import { decodeEvmBytecodeCarrier } from "./EvmBytecodeCarrier.js"; +import { + selectEvmWorkerLimits, + type EvmWorkerLimits, +} from "./EvmWorkerLimits.js"; + +const OPERATION = "inspect_evm_interface"; +const replySchema = z.discriminatedUnion("ok", [ + z.strictObject({ + ok: z.literal(true), + version: z.literal("0.9.3"), + bytecode: z.strictObject({ + sha256: z.string().regex(/^[a-f0-9]{64}$/), + bytes: z.number().int().nonnegative(), + hex: z.string().regex(/^(?:[a-f0-9]{2})*$/), + }), + raw: evmoleInterfaceReplySchema, + }), + z.strictObject({ + ok: z.literal(false), + reason: z.enum(["format", "decoder", "output-limit", "unsupported"]), + message: z.string(), + }), +]); +type Launcher = NonNullable[2]>["launcher"]; + +/** Run unchanged EVMole/WASM in an owned worker; no target, chain or network execution. */ +export class EvmoleInterfaceProvider implements EvmInterfacePort { + constructor( + readonly environment: Readonly = process.env, + readonly launcher?: Launcher, + readonly createRuntime: () => Promise< + Pick + > = () => PrivateRuntimeRoot.create({ prefix: "rea-evm-interface-" }), + ) {} + + /** Bind recovered interface candidates to both original carrier and decoded byte identity. */ + async inspect( + input: InspectEvmInterfaceInput, + options?: ExecutionOptions, + ): Promise> { + let root: Pick | undefined; + let retainedOutput: AnalysisCapturedOutput | undefined; + let phase: "configuration" | "artifact-read" | "worker" = "configuration"; + let workerLimits: EvmWorkerLimits | undefined; + const limiter = + this.environment.REA_EVM_PRLIMIT_COMMAND ?? "/usr/bin/prlimit"; + let result: Result; + try { + if (options?.signal?.aborted) throw new AnalysisCancelledError(OPERATION); + if (process.platform !== "linux" || process.arch !== "x64") + throw new AnalysisCapabilityUnavailableError( + EVMOLE_PROVIDER_IDENTITY.id, + OPERATION, + "Initial real-verified offline EVM interface profile supports Linux x64 only.", + ); + try { + if (!isAbsolute(limiter)) + throw new Error( + "REA_EVM_PRLIMIT_COMMAND must be an absolute executable path.", + ); + await access(limiter, constants.X_OK); + if (!(await stat(limiter)).isFile()) + throw new Error( + "Configured prlimit must be a regular executable file, not a directory.", + ); + } catch (cause: unknown) { + const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${limiter}; ${cause instanceof Error ? cause.message : String(cause)}`; + throw new AnalysisCapabilityUnavailableError( + EVMOLE_PROVIDER_IDENTITY.id, + OPERATION, + reason, + { cause, userMessage: reason }, + ); + } + let inheritedLimits: string; + try { + inheritedLimits = await readFile("/proc/self/limits", "utf8"); + } catch (cause: unknown) { + const reason = `Could not read inherited Linux resource limits at /proc/self/limits: ${cause instanceof Error ? cause.message : String(cause)}`; + throw new AnalysisCapabilityUnavailableError( + EVMOLE_PROVIDER_IDENTITY.id, + OPERATION, + reason, + { cause, userMessage: reason }, + ); + } + const limits = selectEvmWorkerLimits(inheritedLimits); + if (!limits.ok) + throw new AnalysisCapabilityUnavailableError( + EVMOLE_PROVIDER_IDENTITY.id, + OPERATION, + limits.error, + { userMessage: limits.error }, + ); + workerLimits = limits.value; + phase = "artifact-read"; + const carrier = await readStableArtifact( + input.path, + EVM_INTERFACE_LIMITS.inputBytes, + options?.signal, + ); + phase = "worker"; + root = await this.createRuntime(); + const snapshotPath = join(root.path, "carrier.snapshot"); + const requestPath = join(root.path, "request.json"); + const replyPath = join(root.path, "reply.json"); + await writeFile(snapshotPath, carrier.bytes, { + flag: "wx", + mode: 0o600, + ...(options?.signal === undefined ? {} : { signal: options.signal }), + }); + await writeFile( + requestPath, + JSON.stringify({ + snapshot_path: snapshotPath, + reply_path: replyPath, + failure_marker_path: join(root.path, "resource.failure"), + encoding: input.encoding, + }), + { flag: "wx", mode: 0o600 }, + ); + const { NODE_OPTIONS: _ambientNodeOptions, ...environment } = + this.environment; + const execution = await runOwnedCommand( + { + command: limiter, + arguments: [ + `--as=${String(workerLimits.addressSpaceBytes)}:`, + `--fsize=${String(workerLimits.fileSizeBytes)}:`, + "--core=0:", + `--cpu=${String(workerLimits.cpuSeconds)}:`, + "--", + process.execPath, + "--max-old-space-size=256", + "--v8-pool-size=1", + "--disable-wasm-trap-handler", + fileURLToPath( + new URL("./EvmoleInterfaceWorker.js", import.meta.url), + ), + requestPath, + ], + cwd: root.path, + expectedCommand: null, + runId: `rea-evm-interface-${randomUUID()}`, + hostEnvironment: environment, + }, + { + timeoutMs: EVM_INTERFACE_LIMITS.timeoutMs, + diagnosticBytes: EVM_INTERFACE_LIMITS.diagnosticBytes, + }, + { + ...(options?.signal === undefined ? {} : { signal: options.signal }), + ...(this.launcher === undefined ? {} : { launcher: this.launcher }), + }, + ); + const capturedOutput = capturedEvmOutput(execution); + retainedOutput = capturedOutput; + let reply: z.output; + try { + const file = await readStableArtifact( + replyPath, + EVM_INTERFACE_LIMITS.outputBytes, + options?.signal, + ); + reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8"))); + } catch (cause: unknown) { + if (options?.signal?.aborted) + throw new AnalysisCancelledError(OPERATION, { capturedOutput }); + throw new AnalysisOutputError( + OPERATION, + `Owned interface worker reply failed for ${input.path}: ${cause instanceof Error ? cause.message : String(cause)}`, + { cause, capturedOutput }, + ); + } + if (!reply.ok) { + if (reply.reason === "unsupported") + throw new AnalysisUnsupportedTargetError( + OPERATION, + input.path, + reply.message, + { capturedOutput }, + ); + if (reply.reason === "format") + throw new AnalysisInputError(OPERATION, { capturedOutput }, [ + { + path: ["path"], + reason: "invalid_format", + message: reply.message, + }, + ]); + if (reply.reason === "output-limit") + throw new AnalysisOutputError(OPERATION, reply.message, { + capturedOutput, + }); + throw new ProviderAdapterError(EVMOLE_PROVIDER_IDENTITY.id, OPERATION, { + diagnostics: { + phase, + failure_kind: reply.reason, + reason: reply.message, + stdout: execution.stdout.text, + stderr: execution.stderr.text, + captured_output: { ...capturedOutput }, + }, + }); + } + const selectedBytes = decodeEvmBytecodeCarrier( + carrier.bytes, + input.encoding, + ); + const selectedHex = Buffer.from(selectedBytes).toString("hex"); + const decodedHash = createHash("sha256") + .update(selectedBytes) + .digest("hex"); + if ( + decodedHash !== reply.bytecode.sha256 || + selectedHex !== reply.bytecode.hex || + selectedBytes.length !== reply.bytecode.bytes + ) + throw new AnalysisOutputError( + OPERATION, + "Decoded bytecode representation does not match the selected carrier, encoding or reported SHA-256.", + { capturedOutput }, + ); + const limitations = [ + `Configured worker resource soft limits: address_space_bytes=${String(workerLimits.addressSpaceBytes)}, cpu_seconds=${String(workerLimits.cpuSeconds)}, file_size_bytes=${String(workerLimits.fileSizeBytes)}. Selected from inherited tighter limits; original hard limits are retained. Effective worker limits are not independently probed.`, + "Upstream may also report terminal metadata while recovering selectors. Its reported values are retained in raw_result, but integer precision and non-text-key coverage are unknown in the JS binding; complete original bytecode remains available.", + "Recovered selectors, argument strings and mutability are upstream inferences, not verified ABI signatures or names. Empty results do not prove the absence of callable dispatch.", + "Bytecode kind, hardfork and deployed-contract authenticity are unknown. This inspection performs no target runtime execution, chain/RPC lookup or network request.", + "Original carrier and decoded bytecode SHA-256 are distinct identities; SHA-256 is not the Ethereum Keccak code hash.", + "The owned worker has a 256 MiB JavaScript heap, 3 GiB virtual address-space limit and 30-second deadline. Virtual address space is distinct from resident memory; limits fail without partial success.", + ]; + const validated = evmInterfaceSchema.safeParse({ + artifact: { + path: input.path, + sha256: carrier.sha256, + bytes: carrier.bytes.length, + encoding: input.encoding, + }, + bytecode: { + ...reply.bytecode, + digest_algorithm: "sha256", + kind: "unknown", + deployment_authenticity: "unknown", + hardfork: "unknown", + }, + evidence_kind: "inferred", + functions: projectEvmoleInterface(reply.raw).functions, + discovery_completeness: "unknown", + runtime_execution: "not-performed", + diagnostics: { + stdout: execution.stdout.text, + stderr: execution.stderr.text, + truncated: capturedOutput.truncated, + }, + limitations, + }); + if (!validated.success) + throw new AnalysisOutputError( + OPERATION, + `Interface worker returned invalid bytecode-relative evidence: ${validated.error.issues[0]?.message ?? "schema mismatch"}`, + { capturedOutput }, + ); + const report = validated.data; + result = ok( + createAnalysisExecution(report, EVMOLE_PROVIDER_IDENTITY, { + rawResult: reply.raw, + subject: { path: input.path, format: "file", sha256: carrier.sha256 }, + locations: [{ kind: "artifact-path", path: input.path }], + limitations, + }), + ); + } catch (cause: unknown) { + let fileFailure: EvmFileSizeFailureEvidence | undefined; + if ( + cause instanceof OwnedCommandFailure && + cause.snapshot?.exitCode === EVM_FILE_SIZE_FAILURE_EXIT && + cause.snapshot.signal === null && + root !== undefined + ) { + try { + const marker = await readStableArtifact( + join(root.path, "resource.failure"), + 1, + ); + if (!marker.bytes.equals(Buffer.from("F"))) + throw new Error( + "Private worker failure marker does not match the observed exit status.", + ); + fileFailure = { verified: true, failure: null }; + } catch (markerFailure: unknown) { + fileFailure = { + verified: false, + failure: + markerFailure instanceof Error + ? markerFailure.message + : String(markerFailure), + }; + } + } + result = err( + options?.signal?.aborted && + (cause === options.signal.reason || + (cause instanceof Error && cause.name === "AbortError")) + ? new AnalysisCancelledError( + OPERATION, + retainedOutput === undefined + ? undefined + : { capturedOutput: retainedOutput }, + ) + : evmInterfaceFailure( + cause, + phase, + input.path, + workerLimits, + limiter, + fileFailure, + ), + ); + } + if (root !== undefined) { + try { + await root.close(); + } catch (cause: unknown) { + return err( + new ProviderCleanupError( + EVMOLE_PROVIDER_IDENTITY.id, + [root.path], + { + reason: cause instanceof Error ? cause.message : String(cause), + previous_error: result.ok + ? null + : projectAnalysisError(result.error), + ...(retainedOutput === undefined + ? {} + : { captured_output: { ...retainedOutput } }), + }, + { operation: OPERATION }, + ), + ); + } + } + return result.ok && options?.signal?.aborted + ? err( + new AnalysisCancelledError( + OPERATION, + retainedOutput === undefined + ? undefined + : { capturedOutput: retainedOutput }, + ), + ) + : result; + } +} diff --git a/src/evm/EvmoleInterfaceWorker.ts b/src/evm/EvmoleInterfaceWorker.ts new file mode 100644 index 0000000000..061ceb47e7 --- /dev/null +++ b/src/evm/EvmoleInterfaceWorker.ts @@ -0,0 +1,108 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { z } from "zod"; +import { + decodeEvmBytecodeCarrier, + EvmCarrierFailure, +} from "./EvmBytecodeCarrier.js"; +import { projectEvmoleInterface } from "./EvmoleInterfaceAdapter.js"; +import { + EVM_INTERFACE_LIMITS, + EVM_FILE_SIZE_FAILURE_EXIT, +} from "./EvmoleRelease.js"; + +const requestSchema = z.strictObject({ + snapshot_path: z.string(), + reply_path: z.string(), + failure_marker_path: z.string(), + encoding: z.enum(["raw", "hex"]), +}); +class UnsupportedContainer extends Error {} + +const main = async (): Promise => { + const requestPath = process.argv[2]; + if (requestPath === undefined) + throw new Error("Owned worker request path missing."); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + let reply: unknown; + try { + const bytes = decodeEvmBytecodeCarrier( + await readFile(request.snapshot_path), + request.encoding, + ); + if (bytes[0] === 0xef && bytes[1] === 0x00) + throw new UnsupportedContainer( + "Container marker EF00 is outside the verified flat-bytecode interface profile.", + ); + const modulePath = import.meta.resolve("evmole/no_tla"); + const metadata = z + .object({ version: z.literal("0.9.3") }) + .parse( + JSON.parse( + await readFile(new URL("../package.json", modulePath), "utf8"), + ), + ); + const engine = await import("evmole/no_tla"); + engine.initSync({ + module: await readFile( + new URL(import.meta.resolve("evmole/evmole_bg.wasm")), + ), + }); + const inferred = projectEvmoleInterface( + engine.contractInfo(Buffer.from(bytes).toString("hex"), { + selectors: true, + arguments: true, + stateMutability: true, + }), + ); + reply = { + ok: true, + version: metadata.version, + bytecode: { + sha256: createHash("sha256").update(bytes).digest("hex"), + bytes: bytes.length, + hex: Buffer.from(bytes).toString("hex"), + }, + raw: inferred.raw, + }; + } catch (cause: unknown) { + reply = { + ok: false, + reason: + cause instanceof EvmCarrierFailure + ? "format" + : cause instanceof UnsupportedContainer + ? "unsupported" + : "decoder", + message: cause instanceof Error ? cause.message : String(cause), + }; + } + let encoded = JSON.stringify(reply); + if (Buffer.byteLength(encoded) > EVM_INTERFACE_LIMITS.outputBytes) + encoded = JSON.stringify({ + ok: false, + reason: "output-limit", + message: "Complete interface evidence exceeds the 16 MiB reply budget.", + }); + try { + await writeFile(request.reply_path, encoded, { flag: "wx", mode: 0o600 }); + } catch (cause: unknown) { + if (cause instanceof Error && "code" in cause && cause.code === "EFBIG") { + try { + await writeFile(request.failure_marker_path, "F", { + flag: "wx", + mode: 0o600, + }); + } catch (markerFailure: unknown) { + process.stderr.write( + `File-size failure marker unavailable: ${markerFailure instanceof Error ? markerFailure.message : String(markerFailure)}\n`, + ); + } + process.exitCode = EVM_FILE_SIZE_FAILURE_EXIT; + } else throw cause; + } +}; + +await main(); diff --git a/src/evm/EvmoleRelease.ts b/src/evm/EvmoleRelease.ts new file mode 100644 index 0000000000..8e8ea10fc2 --- /dev/null +++ b/src/evm/EvmoleRelease.ts @@ -0,0 +1,16 @@ +/** Unchanged locked npm/WASM profile; provenance is independent of chain deployment authenticity. */ +export const EVMOLE_PROVIDER_IDENTITY = { + id: "evmole-interface", + name: "REA EVM interface adapter", + version: "evmole@0.9.3", +} as const; +/** Worker-reserved status for an observed EFBIG write without a complete reply. */ +export const EVM_FILE_SIZE_FAILURE_EXIT = 76; +/** Complete offline inspection budgets; worker JavaScript heap is bounded separately. */ +export const EVM_INTERFACE_LIMITS = { + inputBytes: 4 * 1024 * 1024, + outputBytes: 16 * 1024 * 1024, + timeoutMs: 30_000, + diagnosticBytes: 1024 * 1024, + addressSpaceBytes: 3 * 1024 * 1024 * 1024, +} as const; diff --git a/src/firmware/FirmwareCommand.ts b/src/firmware/FirmwareCommand.ts index 85abf068fc..025596528f 100644 --- a/src/firmware/FirmwareCommand.ts +++ b/src/firmware/FirmwareCommand.ts @@ -122,6 +122,7 @@ export const runFirmwareCommand = async (context: { runId, cwd: context.cwd, hostEnvironment: context.environment, + signal, env: { TMPDIR: context.cwd, OMP_NUM_THREADS: "1", diff --git a/src/firmware/FirmwareReports.test.ts b/src/firmware/FirmwareReports.test.ts index 8f658a6cc6..2546c80058 100644 --- a/src/firmware/FirmwareReports.test.ts +++ b/src/firmware/FirmwareReports.test.ts @@ -74,6 +74,68 @@ describe("firmware producer boundaries", () => { ]); expect(normalized.coverage).toBe("partial"); }); +}); + +describe("Unblob multi-file extraction diagnostics", () => { + it("retains nested MultiFile extraction failures in partial coverage", () => { + const outputPath = `${context.outputRoot}/multi`; + const childTask = { + path: outputPath, + depth: 1, + blob_id: "multi-id", + is_multi_file: true, + }; + const extractionFailure = { + __typename__: "ExtractorDependencyNotFoundReport", + severity: "ERROR", + dependencies: ["sasquatch"], + }; + const multiFileReport = { + __typename__: "MultiFileReport", + id: "multi-id", + handler_name: "multipart-firmware", + name: "rootfs", + paths: [`${outputPath}/part-a`, `${outputPath}/part-b`], + extraction_reports: [extractionFailure], + }; + const parent = root(); + const normalized = normalizeUnblobReport( + [ + { + ...parent, + reports: parent.reports.filter( + (item) => item.__typename__ !== "UnknownChunkReport", + ), + subtasks: [childTask], + }, + { + task: childTask, + reports: [ + { + __typename__: "StatReport", + path: outputPath, + size: 0, + is_dir: true, + is_file: false, + is_link: false, + link_target: null, + }, + multiFileReport, + ], + subtasks: [], + }, + ], + context, + ); + + expect(normalized.coverage).toBe("partial"); + expect(normalized.diagnostics).toEqual([ + { input_path: "$output/multi", report: multiFileReport }, + ]); + }); +}); + +describe("firmware producer boundaries", () => { it("rejects unknown tasks outside the owned output and disconnected task graphs", () => { const record = root(); for (const path of [ diff --git a/src/firmware/FirmwareReports.ts b/src/firmware/FirmwareReports.ts index 67826d0ad6..3a7e93d48b 100644 --- a/src/firmware/FirmwareReports.ts +++ b/src/firmware/FirmwareReports.ts @@ -43,6 +43,13 @@ const stat = z.object({ is_link: z.boolean(), link_target: z.string().nullable(), }); +const multiFile = z.object({ + id: z.string(), + handler_name: z.string(), + name: z.string(), + paths: z.array(z.string()), + extraction_reports: z.array(jsonValueSchema), +}); const hashes = z.object({ sha256: digestSchema }); const region = z.object({ id: z.string(), @@ -243,6 +250,11 @@ export const normalizeUnblobReport = ( : null, }); } + if (item.__typename__ === "MultiFileReport") { + const parsed = multiFile.parse(item); + if (parsed.extraction_reports.length > 0) + diagnostics.push({ input_path: path, report: item }); + } } } const recordsByPath = new Map( diff --git a/src/generatedMcpToolCatalog.ts b/src/generatedMcpToolCatalog.ts deleted file mode 100644 index 53c8fefe93..0000000000 --- a/src/generatedMcpToolCatalog.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { ToolAnnotations } from "@modelcontextprotocol/server"; -import type { - CapabilityDescriptor, - ProviderIdentity, -} from "./application/AnalysisProvider.js"; -import type { ToolKind } from "./contracts/toolContractTypes.js"; -import type { ToolEffects } from "./contracts/toolEffects.js"; - -interface GeneratedTool { - readonly name: string; - readonly analysisOperation: CapabilityDescriptor["operation"] | null; - readonly title: string; - readonly description: string; - readonly kind: ToolKind; - readonly requiresSession: boolean; - readonly inputSchema: Readonly>; - readonly outputSchema: Readonly>; - readonly annotations: ToolAnnotations; - readonly effects: ToolEffects; -} - -interface GeneratedProvider { - readonly identity: ProviderIdentity; - readonly capabilities: readonly CapabilityDescriptor[]; -} - -interface GeneratedPayload { - readonly catalog: readonly GeneratedTool[]; - readonly auxiliaryProviders: readonly GeneratedProvider[]; -} - -const GENERATED_PAYLOAD_JSON = - '{\n "auxiliaryProviders": [\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inventory_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "extract_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "decode_interface_builder",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_keyed_archive",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_asset_catalog",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n }\n ],\n "identity": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n }\n },\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "observe_native_ui",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": true,\n "mayShowUi": true,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "capture_native_ui_scenario",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "inspect_macho",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "inspect_signature",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "inspect_plist",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "list_architectures",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "demangle_swift",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "Availability and textual formats depend on the installed macOS/Xcode toolchain."\n ],\n "operation": "inspect_native_dispatch_metadata",\n "provider": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n },\n "reason": null\n }\n ],\n "identity": {\n "id": "native-macos",\n "name": "macOS native inspection utilities",\n "version": null\n }\n },\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "This capability inventories PE/CLI identity only; inspect_managed_members admits metadata members, signatures, and CIL anchors.",\n "It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code."\n ],\n "operation": "inspect_managed_artifact",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "This capability decodes PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",\n "It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code."\n ],\n "operation": "inspect_managed_members",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "This capability inventories managed declarations for native boundaries only; native exports, thunks, and addresses require separate native-provider evidence.",\n "It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code."\n ],\n "operation": "inspect_managed_native_boundaries",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "reason": null\n }\n ],\n "identity": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n }\n }\n ],\n "catalog": [\n {\n "analysisOperation": "annotate_native_function",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Atomically update one function name and/or entry comments, read them back, and return a refreshed complete function dossier. Ghidra changes its ephemeral session analysis database only; original executable bytes remain unchanged. Empty comment text clears that comment. Edits remain available to subsequent MCP calls until close; CLI returns the updated dossier before discarding its session. Invalid edits roll back the entire operation. Windows P0 and providers without this capability are unsupported.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "comment": "Analyst observation",\n "name": "entry",\n "procedure": "0x10100"\n }\n ],\n "properties": {\n "comment": {\n "description": "Regular entry comment; empty text clears it",\n "type": "string"\n },\n "inline_comment": {\n "description": "Inline entry comment; empty text clears it",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "procedure": {\n "description": "Value for procedure.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "annotate_native_function",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "annotations": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "comment": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "inline_comment": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "comment",\n "inline_comment"\n ],\n "type": "object"\n },\n "dossier": {\n "additionalProperties": false,\n "properties": {\n "assembly": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "basic_blocks": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "type": "string"\n },\n "start": {\n "type": "string"\n },\n "successors": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "start",\n "end",\n "successors"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "callees": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "callers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "comments": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "comment",\n "inline"\n ],\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "kind",\n "text"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "incoming_references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "conditional": {\n "type": "boolean"\n },\n "data": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "flow": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "primary": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "read": {\n "type": "boolean"\n },\n "terminal": {\n "type": "boolean"\n },\n "type": {\n "type": "string"\n },\n "write": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "type",\n "flow",\n "call",\n "jump",\n "data",\n "read",\n "write",\n "indirect",\n "computed",\n "conditional",\n "terminal",\n "primary",\n "operand_index",\n "external"\n ],\n "type": "object"\n }\n ]\n },\n "source_address": {\n "type": "string"\n },\n "source_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_address": {\n "type": "string"\n },\n "target_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "source_address",\n "target_address",\n "source_procedure",\n "target_procedure",\n "kind"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "default": [],\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "native_api": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "residual_unknowns": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "available",\n "reason",\n "residual_unknowns"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "calling_convention": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-and-stack-variables",\n "compiler-generated-control-flow",\n "pointer-arithmetic",\n "pseudocode"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "jump_tables": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "data_sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "entry_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "entry_size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "provenance": {\n "enum": [\n "ghidra-decompiler-load-table",\n "ghidra-dispatch-block-data-reference",\n "ghidra-aarch64-byte-table",\n "ghidra-aarch64-halfword-table"\n ],\n "type": "string"\n }\n },\n "required": [\n "address",\n "provenance",\n "entry_size_bytes",\n "entry_count",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "default_targets": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "target_address": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_address",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "dispatch_address": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "case_value": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "target_address": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "case_value",\n "target_address",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "dispatch_address",\n "data_sources",\n "mappings",\n "default_targets",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "parameters": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "data_type": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-or-stack-storage",\n "compiler-generated-variable"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "role": {\n "const": "parameter",\n "type": "string"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type",\n "size_bytes",\n "storage",\n "confidence",\n "evidence",\n "decompiler_artifacts",\n "role"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "pseudocode": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "const": "decompiler-generated-non-source",\n "type": "string"\n },\n "compilable": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "classification",\n "compilable"\n ],\n "type": "object"\n },\n "return_type": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "data_type": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-or-stack-storage",\n "compiler-generated-variable"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "null"\n },\n "ordinal": {\n "type": "null"\n },\n "role": {\n "const": "return",\n "type": "string"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type",\n "size_bytes",\n "storage",\n "confidence",\n "evidence",\n "decompiler_artifacts",\n "role"\n ],\n "type": "object"\n },\n "signature_source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "available",\n "provenance",\n "signature_source",\n "calling_convention",\n "return_type",\n "parameters",\n "jump_tables",\n "pseudocode",\n "decompiler_artifacts",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "native_value_flow": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "def_use": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "definition": {\n "minLength": 1,\n "type": "string"\n },\n "input_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "use": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "definition",\n "use",\n "input_index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "effects": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "memory_read",\n "memory_write",\n "conditional_branch",\n "indirect_branch",\n "direct_call",\n "indirect_call"\n ],\n "type": "string"\n },\n "operand_input": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "value_input": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "operation",\n "kind",\n "operand_input",\n "value_input"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "known_omitted_edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "known_omitted_inputs": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "omitted_operations_lower_bound": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "operations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "block_membership": {\n "default": "unavailable",\n "enum": [\n "member",\n "detached",\n "unavailable"\n ],\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "inputs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "constant_hex": {\n "anyOf": [\n {\n "pattern": "^[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "constant",\n "address",\n "register",\n "unique",\n "other"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "size_bytes",\n "location",\n "constant_hex"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "is_dead": {\n "type": "boolean"\n },\n "opcode": {\n "minLength": 1,\n "type": "string"\n },\n "output": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "constant_hex": {\n "anyOf": [\n {\n "pattern": "^[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "constant",\n "address",\n "register",\n "unique",\n "other"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "size_bytes",\n "location",\n "constant_hex"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "id",\n "address",\n "sequence",\n "opcode",\n "is_dead",\n "block_membership",\n "inputs",\n "output"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "parameter_uses": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "input_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "use": {\n "type": "string"\n }\n },\n "required": [\n "ordinal",\n "use",\n "input_index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "parameters": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "data_type": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provenance": {\n "const": "ghidra-high-pcode",\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "operations",\n "def_use",\n "effects",\n "parameters",\n "parameter_uses",\n "truncated",\n "omitted_operations_lower_bound",\n "known_omitted_inputs",\n "known_omitted_edges",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "outgoing_references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "conditional": {\n "type": "boolean"\n },\n "data": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "flow": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "primary": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "read": {\n "type": "boolean"\n },\n "terminal": {\n "type": "boolean"\n },\n "type": {\n "type": "string"\n },\n "write": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "type",\n "flow",\n "call",\n "jump",\n "data",\n "read",\n "write",\n "indirect",\n "computed",\n "conditional",\n "terminal",\n "primary",\n "operand_index",\n "external"\n ],\n "type": "object"\n }\n ]\n },\n "source_address": {\n "type": "string"\n },\n "source_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_address": {\n "type": "string"\n },\n "target_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "source_address",\n "target_address",\n "source_procedure",\n "target_procedure",\n "kind"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "locals": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "description": {\n "type": "string"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "description",\n "provenance"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "signature": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body",\n "signature",\n "locals"\n ],\n "type": "object"\n },\n "pseudocode": {\n "type": "string"\n },\n "referenced_names": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "source_address": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value",\n "source_address"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "referenced_strings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "source_address": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value",\n "source_address"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "procedure",\n "pseudocode",\n "assembly",\n "comments",\n "callers",\n "callees",\n "incoming_references",\n "outgoing_references",\n "referenced_strings",\n "referenced_names",\n "basic_blocks",\n "native_api",\n "native_value_flow",\n "limitations"\n ],\n "type": "object"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "persists_after_close": {\n "const": false,\n "type": "boolean"\n },\n "scope": {\n "const": "session-analysis-database",\n "type": "string"\n },\n "source_bytes_modified": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "scope",\n "source_bytes_modified",\n "persists_after_close"\n ],\n "type": "object"\n }\n },\n "required": [\n "annotations",\n "dossier",\n "effects"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Annotate Native Function"\n },\n {\n "analysisOperation": "inspect_native_load_image",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Verify the provider\'s loaded DOS MZ or explicitly selected COM image against its immutable target snapshot: complete original/modified source-byte digests, header/module file mappings and memory digests, relocations and external entry. Returns measured observations and independent checks with mismatch coordinates. COM verification includes its imposed entry register context and does not claim PSP/runtime coverage. Overlay and uninitialized coverage remain explicit; other formats return unsupported. Does not execute or modify the target.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "inspect_native_load_image",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "observations": {\n "additionalProperties": false,\n "properties": {\n "compiler_spec_id": {\n "minLength": 1,\n "type": "string"\n },\n "default_address_space": {\n "minLength": 1,\n "type": "string"\n },\n "entry_context": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "registers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "value_hex": {\n "anyOf": [\n {\n "pattern": "^0x[a-f0-9]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "value_hex"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "address",\n "registers"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "entry_points": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "executable_format": {\n "minLength": 1,\n "type": "string"\n },\n "image_base": {\n "minLength": 1,\n "type": "string"\n },\n "language_id": {\n "minLength": 1,\n "type": "string"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address_space": {\n "minLength": 1,\n "type": "string"\n },\n "block": {\n "type": "string"\n },\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "initialized": {\n "type": "boolean"\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "loaded": {\n "type": "boolean"\n },\n "overlay": {\n "type": "boolean"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "source_file_index": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "block",\n "start",\n "end",\n "address_space",\n "initialized",\n "loaded",\n "overlay",\n "length",\n "source_file_index",\n "file_offset",\n "sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "metadata_recovery": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "rehydrated-pointer-candidates-and-committed-instance-types",\n "type": "string"\n },\n "frozen_object_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "frozen_objects_annotated": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "frozen_object_candidates",\n "frozen_objects_annotated",\n "basis"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "derived_memory": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "file_offset": {\n "type": "null"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "address",\n "size_bytes",\n "sha256",\n "file_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "diagnostics": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "discovery": {\n "anyOf": [\n {\n "enum": [\n "symbol",\n "signature-heuristic"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "format": {\n "const": "dotnet-nativeaot",\n "type": "string"\n },\n "format_major": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "format_minor": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "header_address": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "method_tables": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "not_applicable"\n ],\n "type": "string"\n },\n "types": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "type"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "format",\n "status",\n "reason",\n "analysis_artifact_sha256",\n "header_address",\n "discovery",\n "format_major",\n "format_minor",\n "method_tables",\n "types",\n "derived_memory",\n "coverage",\n "diagnostics",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relocations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "memory_bytes_hex": {\n "anyOf": [\n {\n "pattern": "^(?:[a-f0-9]{2})*$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "original_bytes_hex": {\n "anyOf": [\n {\n "pattern": "^(?:[a-f0-9]{2})*$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "segment": {\n "anyOf": [\n {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "minLength": 1,\n "type": "string"\n },\n "type": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "values": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "type": "array"\n }\n },\n "required": [\n "address",\n "segment",\n "offset",\n "status",\n "type",\n "values",\n "original_bytes_hex",\n "memory_bytes_hex"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_files": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "modified_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "original_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "size",\n "original_sha256",\n "modified_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "executable_format",\n "language_id",\n "compiler_spec_id",\n "image_base",\n "default_address_space",\n "source_files",\n "mappings",\n "relocations",\n "entry_points",\n "entry_context"\n ],\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unsupported",\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason",\n "observations",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "checks": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "expected": {\n "$ref": "#/$defs/__schema0"\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "matched": {\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "observed": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "name",\n "matched",\n "expected",\n "observed",\n "file_offset",\n "address"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "entry": {\n "additionalProperties": false,\n "properties": {\n "linear_address": {\n "minLength": 1,\n "type": "string"\n },\n "offset": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "relative_segment": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "relative_segment",\n "offset",\n "linear_address"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "dos-mz",\n "dos-com"\n ],\n "type": "string"\n },\n "header_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "load_segment": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "module_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "observations": {\n "additionalProperties": false,\n "properties": {\n "compiler_spec_id": {\n "minLength": 1,\n "type": "string"\n },\n "default_address_space": {\n "minLength": 1,\n "type": "string"\n },\n "entry_context": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "registers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "value_hex": {\n "anyOf": [\n {\n "pattern": "^0x[a-f0-9]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "value_hex"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "address",\n "registers"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "entry_points": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "executable_format": {\n "minLength": 1,\n "type": "string"\n },\n "image_base": {\n "minLength": 1,\n "type": "string"\n },\n "language_id": {\n "minLength": 1,\n "type": "string"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address_space": {\n "minLength": 1,\n "type": "string"\n },\n "block": {\n "type": "string"\n },\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "initialized": {\n "type": "boolean"\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "loaded": {\n "type": "boolean"\n },\n "overlay": {\n "type": "boolean"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "source_file_index": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "block",\n "start",\n "end",\n "address_space",\n "initialized",\n "loaded",\n "overlay",\n "length",\n "source_file_index",\n "file_offset",\n "sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "metadata_recovery": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "rehydrated-pointer-candidates-and-committed-instance-types",\n "type": "string"\n },\n "frozen_object_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "frozen_objects_annotated": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "frozen_object_candidates",\n "frozen_objects_annotated",\n "basis"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "derived_memory": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "file_offset": {\n "type": "null"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "address",\n "size_bytes",\n "sha256",\n "file_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "diagnostics": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "discovery": {\n "anyOf": [\n {\n "enum": [\n "symbol",\n "signature-heuristic"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "format": {\n "const": "dotnet-nativeaot",\n "type": "string"\n },\n "format_major": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "format_minor": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "header_address": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "method_tables": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "not_applicable"\n ],\n "type": "string"\n },\n "types": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "type"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "format",\n "status",\n "reason",\n "analysis_artifact_sha256",\n "header_address",\n "discovery",\n "format_major",\n "format_minor",\n "method_tables",\n "types",\n "derived_memory",\n "coverage",\n "diagnostics",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relocations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "memory_bytes_hex": {\n "anyOf": [\n {\n "pattern": "^(?:[a-f0-9]{2})*$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "original_bytes_hex": {\n "anyOf": [\n {\n "pattern": "^(?:[a-f0-9]{2})*$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "segment": {\n "anyOf": [\n {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "minLength": 1,\n "type": "string"\n },\n "type": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "values": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "type": "array"\n }\n },\n "required": [\n "address",\n "segment",\n "offset",\n "status",\n "type",\n "values",\n "original_bytes_hex",\n "memory_bytes_hex"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_files": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "modified_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "original_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "size",\n "original_sha256",\n "modified_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "executable_format",\n "language_id",\n "compiler_spec_id",\n "image_base",\n "default_address_space",\n "source_files",\n "mappings",\n "relocations",\n "entry_points",\n "entry_context"\n ],\n "type": "object"\n },\n "overlay_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "verified",\n "mismatch"\n ],\n "type": "string"\n },\n "target_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "status",\n "format",\n "target_sha256",\n "load_segment",\n "header_bytes",\n "module_bytes",\n "overlay_bytes",\n "entry",\n "observations",\n "checks",\n "limitations"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Native Load Image"\n },\n {\n "analysisOperation": "inspect_native_data_type",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect one recovered type by exact database category path or defined typed data address. Returns observed size, alignment, packing, fields, bitfields, enum values, and child type identities; source-level authority and flexible-tail semantics remain unknown unless substantiated.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "type": "/MyStruct"\n }\n ],\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "document": {\n "description": "Exact provider document or program identity.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "inspect_native_data_type",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "alignment_bytes": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "array_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "array_stride_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "fields": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "bit_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "bit_size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "flexible_tail": {\n "const": "unknown",\n "type": "string"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "offset_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "observed",\n "unsupported"\n ],\n "type": "string"\n },\n "type_id": {\n "type": "string"\n }\n },\n "required": [\n "ordinal",\n "name",\n "offset_bytes",\n "size_bytes",\n "type_id",\n "bit_size",\n "bit_offset",\n "status",\n "flexible_tail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "struct",\n "union",\n "enum",\n "pointer",\n "array",\n "typedef",\n "scalar",\n "unsupported",\n "unavailable"\n ],\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "members": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "metadata_recovery": {\n "additionalProperties": false,\n "properties": {\n "base_size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "derived_memory": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "file_offset": {\n "type": "null"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "address",\n "size_bytes",\n "sha256",\n "file_offset"\n ],\n "type": "object"\n },\n "diagnostics": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "format": {\n "const": "dotnet-nativeaot",\n "type": "string"\n },\n "format_major": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "format_minor": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "header_address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "interfaces": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "method_table_address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "name_origin": {\n "enum": [\n "generated",\n "inferred"\n ],\n "type": "string"\n },\n "original_name": {\n "type": "null"\n },\n "related_type": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "type"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n },\n "virtual_slots": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "method-table-pointer",\n "type": "string"\n },\n "procedure_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "slot": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "slot_address": {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n "target_address": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "slot",\n "slot_address",\n "target_address",\n "procedure_name",\n "basis"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "format",\n "status",\n "header_address",\n "format_major",\n "format_minor",\n "method_table_address",\n "name_origin",\n "original_name",\n "base_size_bytes",\n "related_type",\n "interfaces",\n "virtual_slots",\n "derived_memory",\n "diagnostics",\n "limitations"\n ],\n "type": "object"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "packing_enabled": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "referenced_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "source": {\n "const": "analysis-database",\n "type": "string"\n },\n "source_archive": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "available",\n "unavailable"\n ],\n "type": "string"\n },\n "total_fields": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "reason",\n "id",\n "name",\n "kind",\n "source",\n "source_archive",\n "address",\n "size_bytes",\n "alignment_bytes",\n "packing_enabled",\n "referenced_type",\n "array_count",\n "array_stride_bytes",\n "fields",\n "members",\n "total_fields",\n "truncated",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Native Data Type"\n },\n {\n "analysisOperation": "inspect_native_instruction",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect one exact instruction address: decoded bytes, mnemonic, ordered operand tokens, flow and typed references. Memory addressing decomposition is unavailable when the provider supplies only tokens. Data, interior instruction addresses and undecodable bytes are distinct outcomes.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "document": {\n "description": "Exact provider document or program identity.",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "inspect_native_instruction",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "architecture": {\n "type": "string"\n },\n "bytes": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "flow": {\n "additionalProperties": false,\n "properties": {\n "computed": {\n "type": "boolean"\n },\n "conditional": {\n "type": "boolean"\n },\n "direct_destinations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "enum": [\n "call",\n "jump",\n "return",\n "fallthrough",\n "terminal",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "conditional",\n "computed",\n "direct_destinations"\n ],\n "type": "object"\n },\n "length": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "mnemonic": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "mode": {\n "type": "string"\n },\n "operands": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "components": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "bit_width": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "register",\n "immediate",\n "address",\n "unknown"\n ],\n "type": "string"\n },\n "text": {\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "text",\n "value",\n "bit_width"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "memory_addressing": {\n "const": "unavailable",\n "type": "string"\n },\n "provider_type": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "raw": {\n "type": "string"\n }\n },\n "required": [\n "index",\n "raw",\n "provider_type",\n "components",\n "memory_addressing"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "procedure": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "raw_disassembly": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "target_address": {\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "target_address",\n "type",\n "call",\n "jump",\n "indirect",\n "computed",\n "operand_index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "decoded",\n "not-instruction-boundary",\n "data",\n "undecodable",\n "outside-memory"\n ],\n "type": "string"\n }\n },\n "required": [\n "address",\n "status",\n "procedure",\n "architecture",\n "mode",\n "bytes",\n "length",\n "mnemonic",\n "raw_disassembly",\n "operands",\n "flow",\n "references",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Native Instruction"\n },\n {\n "analysisOperation": "resolve_native_call_targets",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Resolve one explicit static call site using typed provider call references. Preserves ambiguous targets as candidates and unresolved computed calls as unknown; does not execute code or traverse a call graph.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "document": {\n "description": "Exact provider document or program identity.",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "resolve_native_call_targets",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "call_site": {\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "mechanism": {\n "enum": [\n "direct",\n "computed",\n "indirect",\n "unavailable"\n ],\n "type": "string"\n },\n "procedure": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "direct",\n "resolved-indirect",\n "ambiguous",\n "unresolved",\n "not-call",\n "unavailable"\n ],\n "type": "string"\n },\n "targets": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "basis": {\n "const": "provider-reference",\n "type": "string"\n },\n "procedure": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "target_address": {\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "target_address",\n "type",\n "call",\n "jump",\n "indirect",\n "computed",\n "operand_index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "direct",\n "resolved-indirect",\n "candidate"\n ],\n "type": "string"\n }\n },\n "required": [\n "address",\n "procedure",\n "status",\n "basis",\n "references"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "call_site",\n "procedure",\n "status",\n "mechanism",\n "targets",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Resolve Native Call Targets"\n },\n {\n "analysisOperation": "address_name",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Resolve the primary analyzed name at a code or data address. Headless providers require an explicit address; GUI providers may default to their current cursor. Null means the provider has no primary name at that address.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "address_name",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Address Name"\n },\n {\n "analysisOperation": "comment",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Read the regular analysis comment at an address, defaulting to the current cursor. This is read-only and returns null when no comment exists; use set_comment to persist a finding.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "comment",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Comment"\n },\n {\n "analysisOperation": "current_address",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return Hopper\'s current cursor address for the selected document. Use only for interactive navigation state; prefer explicit addresses in reproducible investigations.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "current_address",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Current Address"\n },\n {\n "analysisOperation": "current_procedure",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the analyzed procedure containing Hopper\'s current cursor. This uses GUI cursor state and is not a procedure-name lookup.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "current_procedure",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Current Procedure"\n },\n {\n "analysisOperation": "current_document",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the document currently selected by REA\'s Hopper bridge.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "current_document",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Current Document"\n },\n {\n "analysisOperation": "goto_address",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Move Hopper\'s GUI cursor to a hexadecimal address and return the resolved address. This changes navigation state but not analysis data; use explicit-address tools for headless workflows.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": true,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "goto_address",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Goto Address"\n },\n {\n "analysisOperation": "inline_comment",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Read the inline instruction comment at an address, defaulting to the current cursor. Returns null when absent; use set_inline_comment to write one.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "inline_comment",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inline Comment"\n },\n {\n "analysisOperation": "list_bookmarks",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "List every bookmark in the selected Hopper document as address and name pairs. Use bookmarks as analyst-authored navigation aids; this does not discover code references.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "list_bookmarks",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Bookmarks"\n },\n {\n "analysisOperation": "list_documents",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "List provider program or document identities. Hopper may expose several documents; a Ghidra headless session contains exactly its one imported Program.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "list_documents",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Documents"\n },\n {\n "analysisOperation": "list_names",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "List every analyzed memory and external symbol as address/value pairs. Provider metadata distinguishes Ghidra primary, dynamic, external, type, and source facts when available.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "list_names",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target"\n ],\n "type": "object"\n },\n "string": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "encoding": {\n "type": "string"\n },\n "termination": {\n "enum": [\n "missing",\n "present_or_not_required"\n ],\n "type": "string"\n }\n },\n "required": [\n "encoding",\n "termination",\n "byte_length"\n ],\n "type": "object"\n },\n "symbol": {\n "additionalProperties": false,\n "properties": {\n "dynamic": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "primary": {\n "type": "boolean"\n },\n "source": {\n "enum": [\n "default",\n "analysis",\n "ai",\n "imported",\n "user_defined"\n ],\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "primary",\n "dynamic",\n "external",\n "type",\n "source"\n ],\n "type": "object"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Names"\n },\n {\n "analysisOperation": "list_procedures",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "List every analyzed procedure as address/value pairs after provider analysis. Ghidra metadata distinguishes thunks and external functions; use returned addresses in later function operations.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "list_procedures",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target"\n ],\n "type": "object"\n },\n "string": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "encoding": {\n "type": "string"\n },\n "termination": {\n "enum": [\n "missing",\n "present_or_not_required"\n ],\n "type": "string"\n }\n },\n "required": [\n "encoding",\n "termination",\n "byte_length"\n ],\n "type": "object"\n },\n "symbol": {\n "additionalProperties": false,\n "properties": {\n "dynamic": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "primary": {\n "type": "boolean"\n },\n "source": {\n "enum": [\n "default",\n "analysis",\n "ai",\n "imported",\n "user_defined"\n ],\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "primary",\n "dynamic",\n "external",\n "type",\n "source"\n ],\n "type": "object"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Procedures"\n },\n {\n "analysisOperation": "list_segments",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "List segments or memory blocks using exclusive end addresses. Ghidra reports block permissions, address space, image base, initialization, and overlay facts; Hopper marks unavailable permissions explicitly.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "list_segments",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address_space": {\n "type": "string"\n },\n "end": {\n "type": "string"\n },\n "executable": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "image_base": {\n "type": "string"\n },\n "initialized": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "overlay": {\n "type": "boolean"\n },\n "permissions": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "source": {\n "const": "ghidra-memory-block",\n "type": "string"\n }\n },\n "required": [\n "available",\n "source"\n ],\n "type": "object"\n }\n ]\n },\n "provenance": {\n "enum": [\n "hopper-public-python-api",\n "ghidra-memory-block"\n ],\n "type": "string"\n },\n "readable": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "sections": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address_space": {\n "type": "string"\n },\n "end": {\n "type": "string"\n },\n "executable": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "image_base": {\n "type": "string"\n },\n "initialized": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "overlay": {\n "type": "boolean"\n },\n "permissions": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "source": {\n "const": "ghidra-memory-block",\n "type": "string"\n }\n },\n "required": [\n "available",\n "source"\n ],\n "type": "object"\n }\n ]\n },\n "provenance": {\n "enum": [\n "hopper-public-python-api",\n "ghidra-memory-block"\n ],\n "type": "string"\n },\n "readable": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "start": {\n "type": "string"\n },\n "writable": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "start",\n "end",\n "readable",\n "writable",\n "executable",\n "permissions",\n "provenance"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "start": {\n "type": "string"\n },\n "writable": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "start",\n "end",\n "readable",\n "writable",\n "executable",\n "permissions",\n "provenance",\n "sections"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Segments"\n },\n {\n "analysisOperation": "list_strings",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "List every provider-defined string, or filter to one address, as address/value pairs. Ghidra also reports encoding, terminator status, and byte length.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "list_strings",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "string": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "encoding": {\n "minLength": 1,\n "type": "string"\n },\n "termination": {\n "enum": [\n "missing",\n "present_or_not_required"\n ],\n "type": "string"\n }\n },\n "required": [\n "encoding",\n "termination",\n "byte_length"\n ],\n "type": "object"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Strings"\n },\n {\n "analysisOperation": "next_address",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the next analyzed object address after an explicit address or current cursor. This is a navigation primitive, not instruction-flow or CFG analysis.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "next_address",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Next Address"\n },\n {\n "analysisOperation": "prev_address",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the previous analyzed instruction start before an explicit address or current cursor. This is a navigation primitive and may fail at document boundaries.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "prev_address",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Prev Address"\n },\n {\n "analysisOperation": "procedure_address",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Resolve an unambiguous procedure symbol name or provider-normalized address to its canonical entry address. External address spaces remain explicit.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_address",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure Address"\n },\n {\n "analysisOperation": "procedure_assembly",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return assembly for one analyzed procedure identified by symbol or hexadecimal address. Use when pseudocode loses calling-convention or instruction-level detail; output is currently returned as one unpaginated string.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_assembly",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure Assembly"\n },\n {\n "analysisOperation": "procedure_callees",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the provider\'s resolved direct callees for one procedure identified by symbol or address. Unresolved indirect calls may be absent; use analyze_function and typed references to preserve available edge uncertainty.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_callees",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure Callees"\n },\n {\n "analysisOperation": "procedure_callers",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the provider\'s resolved direct callers for one procedure identified by symbol or address. Results reflect completed static analysis and may omit unresolved indirect references.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_callers",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure Callers"\n },\n {\n "analysisOperation": "procedure_info",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return provider metadata for one procedure identified by symbol or address: entrypoint, signature, locals, size, block count, and complete inclusive body ranges when observed. Body byte count and enclosing span remain distinct; unavailable extent is explicit.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_info",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "basicblock_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "entrypoint": {\n "type": "string"\n },\n "length": {\n "minimum": 0,\n "type": "number"\n },\n "locals": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "description": {\n "type": "string"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "description",\n "provenance"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "signature": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "entrypoint",\n "basicblock_count",\n "length",\n "signature",\n "locals",\n "classification",\n "body"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure Info"\n },\n {\n "analysisOperation": "read_function_instructions",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return every raw instruction for one analyzed procedure. Instruction text is provider-specific.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "read_function_instructions",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "instructions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n }\n },\n "required": [\n "procedure",\n "instructions",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Read Function Instructions"\n },\n {\n "analysisOperation": "read_bytes",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Read analyzed bytes from one provider-normalized virtual address. The hexadecimal payload reports the exact returned length; incomplete reads remain explicit and unsupported provider APIs return typed capability unavailability.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000",\n "length": 16\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "length": {\n "default": 256,\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "read_bytes",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "bytes_hex": {\n "pattern": "^(?:[a-f0-9]{2})*$",\n "type": "string"\n },\n "complete": {\n "type": "boolean"\n },\n "requested_bytes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "returned_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "address",\n "requested_bytes",\n "returned_bytes",\n "bytes_hex",\n "complete"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Read Bytes"\n },\n {\n "analysisOperation": "address_to_file_offset",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Map one provider-normalized virtual address to its original nonnegative file offset. Unmapped addresses fail explicitly, and providers without an authoritative mapping API return typed capability unavailability.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "address_to_file_offset",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "file_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Address To File Offset"\n },\n {\n "analysisOperation": "procedure_references",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return every raw incoming or outgoing reference edge for one procedure. Endpoint procedures are resolved only from provider containment; Ghidra preserves observed reference kinds while providers without kind authority mark them unavailable.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "direction": "outgoing",\n "procedure": "main"\n }\n ],\n "properties": {\n "direction": {\n "default": "outgoing",\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "incoming",\n "outgoing"\n ],\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_references",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "direction": {\n "enum": [\n "incoming",\n "outgoing"\n ],\n "type": "string"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "reference_kinds_available": {\n "type": "boolean"\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "conditional": {\n "type": "boolean"\n },\n "data": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "flow": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "primary": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "read": {\n "type": "boolean"\n },\n "terminal": {\n "type": "boolean"\n },\n "type": {\n "type": "string"\n },\n "write": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "type",\n "flow",\n "call",\n "jump",\n "data",\n "read",\n "write",\n "indirect",\n "computed",\n "conditional",\n "terminal",\n "primary",\n "operand_index",\n "external"\n ],\n "type": "object"\n }\n ]\n },\n "source_address": {\n "type": "string"\n },\n "source_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_address": {\n "type": "string"\n },\n "target_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "source_address",\n "target_address",\n "source_procedure",\n "target_procedure",\n "kind"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "unresolved_calls": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "procedure",\n "direction",\n "unresolved_calls",\n "references"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure References"\n },\n {\n "analysisOperation": "procedure_pseudo_code",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Decompile one analyzed procedure by symbol name or provider-normalized address. Returns provider-specific pseudocode, never original source or cross-provider text equivalence, and may return null; request procedure_assembly when instruction precision matters.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "procedure": {\n "description": "The procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "procedure_pseudo_code",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Procedure Pseudo Code"\n },\n {\n "analysisOperation": "resolve_containing_procedure",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Resolve an arbitrary address, including an interior instruction or exact external entry, to its provider-analyzed containing procedure. A negative result distinguishes outside segments from not in a procedure and is never guessed from nearby symbols.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "resolve_containing_procedure",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "found": {\n "const": true,\n "type": "boolean"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "query_address": {\n "type": "string"\n }\n },\n "required": [\n "query_address",\n "found",\n "procedure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "found": {\n "const": false,\n "type": "boolean"\n },\n "procedure": {\n "type": "null"\n },\n "query_address": {\n "type": "string"\n },\n "reason": {\n "enum": [\n "outside_segments",\n "not_in_procedure"\n ],\n "type": "string"\n }\n },\n "required": [\n "query_address",\n "found",\n "procedure",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Resolve Containing Procedure"\n },\n {\n "analysisOperation": "search_procedures",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Search every analyzed procedure name using literal matching by default or regex when requested. Results are deterministic and complete.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "case_sensitive": false,\n "mode": "literal",\n "pattern": "main"\n }\n ],\n "properties": {\n "case_sensitive": {\n "default": false,\n "description": "Whether to match case",\n "type": "boolean"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "mode": {\n "default": "literal",\n "description": "Operation mode that selects the requested behavior.",\n "enum": [\n "literal",\n "regex"\n ],\n "type": "string"\n },\n "pattern": {\n "description": "The literal text or regex pattern to search for",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "pattern"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "search_procedures",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Search Procedures"\n },\n {\n "analysisOperation": "search_strings",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Search every analyzed string using literal matching by default or regex when requested. Results are deterministic and complete.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "case_sensitive": false,\n "mode": "literal",\n "pattern": "authorization failed"\n }\n ],\n "properties": {\n "case_sensitive": {\n "default": false,\n "description": "Whether to match case",\n "type": "boolean"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "mode": {\n "default": "literal",\n "description": "Operation mode that selects the requested behavior.",\n "enum": [\n "literal",\n "regex"\n ],\n "type": "string"\n },\n "pattern": {\n "description": "The literal text or regex pattern to search for",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "pattern"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "search_strings",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Search Strings"\n },\n {\n "analysisOperation": "set_address_name",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Assign an analyst name to one hexadecimal address and report Hopper\'s boolean result. This mutates analysis metadata.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000",\n "name": "entry"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "set_address_name",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "boolean"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Set Address Name"\n },\n {\n "analysisOperation": "set_addresses_names",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Assign analyst names to multiple addresses in one call and return per-address success booleans. This mutates analysis metadata; verify failures individually.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "names": {\n "0x1000": "entry"\n }\n }\n ],\n "properties": {\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "names": {\n "additionalProperties": {\n "type": "string"\n },\n "description": "Value for names.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "names"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "set_addresses_names",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": {\n "type": "boolean"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Set Addresses Names"\n },\n {\n "analysisOperation": "set_bookmark",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Create or replace a bookmark at a hexadecimal address and report success. This mutates navigation metadata; bookmarks are analyst-authored navigation aids, not binary evidence.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "set_bookmark",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "boolean"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Set Bookmark"\n },\n {\n "analysisOperation": "set_comment",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Write a regular analysis comment at a hexadecimal address and return whether readback matched. This mutates the Hopper document; use comments to record evidence IDs or reasoning.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000",\n "comment": "validated entry point"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "comment": {\n "description": "Exact analyst comment text to write.",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "required": [\n "address",\n "comment"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "set_comment",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "boolean"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Set Comment"\n },\n {\n "analysisOperation": "set_current_document",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Select an already-open Hopper document by exact document name. This changes subsequent default-document routing; list_documents can supply names when needed, while explicit document inputs keep calls reproducible.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": true,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "document": "fixture"\n }\n ],\n "properties": {\n "document": {\n "description": "Exact provider document or program identity.",\n "type": "string"\n }\n },\n "required": [\n "document"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "set_current_document",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "string"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Set Current Document"\n },\n {\n "analysisOperation": "set_inline_comment",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Write an inline instruction comment at a hexadecimal address and return whether readback matched. This mutates analysis metadata.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000",\n "comment": "calls parser"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "comment": {\n "description": "Exact analyst comment text to write.",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "required": [\n "address",\n "comment"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "set_inline_comment",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "boolean"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Set Inline Comment"\n },\n {\n "analysisOperation": "unset_bookmark",\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Remove the bookmark at a hexadecimal address and return whether it is absent. This mutates navigation metadata and does not alter binary bytes.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": true,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "unset_bookmark",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "type": "boolean"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Unset Bookmark"\n },\n {\n "analysisOperation": "xrefs",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized address; default memory uses 0x-prefixed hexadecimal",\n "type": "string"\n },\n "document": {\n "description": "The document name",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "official-proxy",\n "name": "xrefs",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Xrefs"\n },\n {\n "analysisOperation": "inspect_native_dispatch_metadata",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect the bound provider\'s name inventory for Objective-C class/method symbols and Swift mangled symbols, returning at most max_records decoded names. Includes target SHA-256 when available, provider identity/version, analysis-profile digest, facet coverage, and per-record Evidence. This is symbol-level evidence, not runtime metadata: ivar layouts, protocol conformances, witness/class tables, and relative pointers are explicitly marked unsupported unless a provider supplies those decoders. The provider controls how its name inventory is acquired.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "max_records": 5000\n }\n ],\n "properties": {\n "max_records": {\n "default": 5000,\n "description": "Maximum permitted records for this operation.",\n "maximum": 20000,\n "minimum": 1,\n "type": "integer"\n }\n },\n "type": "object"\n },\n "kind": "enhanced",\n "name": "inspect_native_dispatch_metadata",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile_digest": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "decoded": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "examined": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "facet": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "unsupported",\n "not_requested"\n ],\n "type": "string"\n }\n },\n "required": [\n "facet",\n "status",\n "reason",\n "examined",\n "decoded"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "db_save_result": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "database_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "error": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "operation": {\n "enum": [\n "save",\n "readback",\n "open_database",\n "close_database",\n "export",\n "import"\n ],\n "type": "string"\n },\n "preserved_bookmarks": {\n "default": 0,\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "preserved_comments": {\n "default": 0,\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "preserved_names": {\n "default": 0,\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "succeeded": {\n "type": "boolean"\n }\n },\n "required": [\n "operation",\n "succeeded",\n "preserved_names",\n "preserved_comments",\n "preserved_bookmarks",\n "database_path",\n "error"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "objc_categories": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "class_methods": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "class_name": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "class_source": {\n "enum": [\n "local",\n "external",\n "unresolved"\n ],\n "type": "string"\n },\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "instance_methods": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "properties": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attributes": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "is_atomic": {\n "default": false,\n "type": "boolean"\n },\n "is_copy": {\n "default": false,\n "type": "boolean"\n },\n "is_strong": {\n "default": false,\n "type": "boolean"\n },\n "is_weak": {\n "default": false,\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "value",\n "is_weak",\n "is_atomic",\n "is_copy",\n "is_strong"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "getter": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_readonly": {\n "default": false,\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "setter": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "type_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "type_encoding",\n "attributes",\n "is_readonly",\n "getter",\n "setter"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "protocols": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "class_name",\n "class_source",\n "instance_methods",\n "class_methods",\n "protocols",\n "properties",\n "location",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "objc_classes": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "type": "string"\n },\n "artifact_sha256": {\n "type": "string"\n },\n "description": {\n "type": "string"\n },\n "kind": {\n "const": "binary_metadata",\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "instance_size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_meta_class": {\n "default": false,\n "type": "boolean"\n },\n "is_root_class": {\n "default": false,\n "type": "boolean"\n },\n "ivar_count": {\n "default": 0,\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "metaclass_address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "methods": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_optional": {\n "default": false,\n "type": "boolean"\n },\n "is_required": {\n "default": false,\n "type": "boolean"\n },\n "method_type": {\n "enum": [\n "instance",\n "class",\n "ivar_getter",\n "ivar_setter"\n ],\n "type": "string"\n },\n "selector": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "selector",\n "method_type",\n "address",\n "is_required",\n "is_optional"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "properties": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "attributes": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "is_atomic": {\n "default": false,\n "type": "boolean"\n },\n "is_copy": {\n "default": false,\n "type": "boolean"\n },\n "is_strong": {\n "default": false,\n "type": "boolean"\n },\n "is_weak": {\n "default": false,\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "value",\n "is_weak",\n "is_atomic",\n "is_copy",\n "is_strong"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "getter": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_readonly": {\n "default": false,\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "setter": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "type_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "type_encoding",\n "attributes",\n "is_readonly",\n "getter",\n "setter"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "protocols": {\n "default": [],\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "super_class": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "superclass_address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "super_class",\n "is_meta_class",\n "is_root_class",\n "methods",\n "properties",\n "protocols",\n "ivar_count",\n "instance_size"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "objc_dispatch_implementations": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "category": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "class_name": {\n "minLength": 1,\n "type": "string"\n },\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "implementation_address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "method_type": {\n "enum": [\n "instance",\n "class"\n ],\n "type": "string"\n },\n "selector": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "class_name",\n "selector",\n "method_type",\n "implementation_address",\n "location",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "objc_ivars": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "alignment": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "class_name": {\n "minLength": 1,\n "type": "string"\n },\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "type_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "class_name",\n "name",\n "type_encoding",\n "offset",\n "size",\n "alignment",\n "location",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "objc_protocol_records": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "adopted_protocols": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "methods": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_optional": {\n "default": false,\n "type": "boolean"\n },\n "is_required": {\n "default": false,\n "type": "boolean"\n },\n "method_type": {\n "enum": [\n "instance",\n "class",\n "ivar_getter",\n "ivar_setter"\n ],\n "type": "string"\n },\n "selector": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "selector",\n "method_type",\n "address",\n "is_required",\n "is_optional"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "optional_methods": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_optional": {\n "default": false,\n "type": "boolean"\n },\n "is_required": {\n "default": false,\n "type": "boolean"\n },\n "method_type": {\n "enum": [\n "instance",\n "class",\n "ivar_getter",\n "ivar_setter"\n ],\n "type": "string"\n },\n "selector": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "selector",\n "method_type",\n "address",\n "is_required",\n "is_optional"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "adopted_protocols",\n "methods",\n "optional_methods",\n "location",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "objc_protocols": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "methods": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_optional": {\n "default": false,\n "type": "boolean"\n },\n "is_required": {\n "default": false,\n "type": "boolean"\n },\n "method_type": {\n "enum": [\n "instance",\n "class",\n "ivar_getter",\n "ivar_setter"\n ],\n "type": "string"\n },\n "selector": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "selector",\n "method_type",\n "address",\n "is_required",\n "is_optional"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "optional_methods": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_optional": {\n "default": false,\n "type": "boolean"\n },\n "is_required": {\n "default": false,\n "type": "boolean"\n },\n "method_type": {\n "enum": [\n "instance",\n "class",\n "ivar_getter",\n "ivar_setter"\n ],\n "type": "string"\n },\n "selector": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "selector",\n "method_type",\n "address",\n "is_required",\n "is_optional"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "properties": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "attributes": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "is_atomic": {\n "default": false,\n "type": "boolean"\n },\n "is_copy": {\n "default": false,\n "type": "boolean"\n },\n "is_strong": {\n "default": false,\n "type": "boolean"\n },\n "is_weak": {\n "default": false,\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "value",\n "is_weak",\n "is_atomic",\n "is_copy",\n "is_strong"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "getter": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_readonly": {\n "default": false,\n "type": "boolean"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "setter": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "type_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "type_encoding",\n "attributes",\n "is_readonly",\n "getter",\n "setter"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "methods",\n "optional_methods",\n "properties"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relative_pointers": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "displacement": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "field": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "indirect": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "indirectable": {\n "type": "boolean"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "field",\n "target",\n "displacement",\n "indirectable",\n "indirect",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "swift_conformances": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "module": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "protocol_name": {\n "minLength": 1,\n "type": "string"\n },\n "type_name": {\n "minLength": 1,\n "type": "string"\n },\n "witness_table": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "type_name",\n "protocol_name",\n "module",\n "witness_table",\n "location",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "swift_decls": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "access_level": {\n "default": "internal",\n "enum": [\n "private",\n "fileprivate",\n "internal",\n "public",\n "open"\n ],\n "type": "string"\n },\n "is_convenience_init": {\n "default": false,\n "type": "boolean"\n },\n "is_final": {\n "default": false,\n "type": "boolean"\n },\n "is_override": {\n "default": false,\n "type": "boolean"\n },\n "is_required": {\n "default": false,\n "type": "boolean"\n },\n "kind": {\n "enum": [\n "class",\n "struct",\n "enum",\n "protocol",\n "extension",\n "actor",\n "global_var",\n "global_func"\n ],\n "type": "string"\n },\n "module": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "protocols": {\n "default": [],\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "super_class": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "name",\n "module",\n "access_level",\n "super_class",\n "protocols",\n "is_final",\n "is_required",\n "is_convenience_init",\n "is_override"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "swift_dispatch_slots": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "implementation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "implementation_address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n "owner": {\n "minLength": 1,\n "type": "string"\n },\n "requirement": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "slot_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "table_kind": {\n "enum": [\n "witness_table",\n "class_vtable"\n ],\n "type": "string"\n },\n "thunk_address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "owner",\n "table_kind",\n "slot_index",\n "requirement",\n "implementation",\n "implementation_address",\n "thunk_address",\n "location",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "swift_symbols": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "decode": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "mangled_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mangled_name",\n "address",\n "decode",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "objc_classes",\n "objc_protocols",\n "objc_categories",\n "swift_decls",\n "objc_ivars",\n "objc_protocol_records",\n "objc_dispatch_implementations",\n "swift_conformances",\n "swift_dispatch_slots",\n "swift_symbols",\n "relative_pointers",\n "coverage",\n "db_save_result"\n ],\n "type": "object"\n },\n "target_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "target_sha256",\n "provider",\n "analysis_profile_digest",\n "result"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Native Dispatch Metadata"\n },\n {\n "analysisOperation": "get_objc_classes",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Discover and deduplicate Objective-C class labels, optionally filtering by literal substring.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "pattern": ""\n }\n ],\n "properties": {\n "pattern": {\n "default": "",\n "description": "Literal text or regular expression used to filter matching results.",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "enhanced",\n "name": "get_objc_classes",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "classes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "count",\n "classes"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Get Objc Classes"\n },\n {\n "analysisOperation": "get_objc_protocols",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Discover and deduplicate Objective-C and Swift protocol labels.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "enhanced",\n "name": "get_objc_protocols",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "protocols": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "count",\n "protocols"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Get Objc Protocols"\n },\n {\n "analysisOperation": "batch_decompile",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Decompile each explicit procedure symbol or address concurrently. Returns ordered per-item success or error results and aggregate counts.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "addresses": []\n }\n ],\n "properties": {\n "addresses": {\n "default": [],\n "description": "Ordered provider-normalized procedure addresses to analyze.",\n "items": {\n "description": "A provider-normalized procedure address",\n "type": "string"\n },\n "type": "array"\n }\n },\n "type": "object"\n },\n "kind": "enhanced",\n "name": "batch_decompile",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n },\n "__schema1": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema1"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema1"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema1"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "failed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "items": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "pseudocode": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "ok",\n "type": "string"\n }\n },\n "required": [\n "address",\n "status",\n "pseudocode"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "error": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "invalid_request",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "unreadable_output",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "capability_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "provider_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "provider_timeout",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "cancelled",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "artifact_integrity_mismatch",\n "type": "string"\n },\n "details": {\n "additionalProperties": false,\n "properties": {\n "calculated_sha256": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declared_sha256": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "logical_path": {\n "type": "string"\n },\n "unpacked": {\n "type": "boolean"\n }\n },\n "required": [\n "logical_path",\n "declared_sha256",\n "calculated_sha256",\n "unpacked"\n ],\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation",\n "details"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "artifact_operation_failed",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "evidence_integrity_mismatch",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "truncated",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "process_capture_failed",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "cleanup_incomplete",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "revision_conflict",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "configuration_invalid",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "target_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "execution_failure",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "status": {\n "const": "error",\n "type": "string"\n }\n },\n "required": [\n "address",\n "status",\n "error"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "succeeded": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "items",\n "total",\n "succeeded",\n "failed"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Batch Decompile"\n },\n {\n "analysisOperation": "get_call_graph",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Traverse the bound provider\'s caller or callee relationships from one symbol or address until the reachable graph is exhausted. Every node has an ok/error status and failures use safe typed projections; unresolved indirect calls may be missing and results are not a whole-program CFG.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "address": "0x1000",\n "direction": "forward"\n }\n ],\n "properties": {\n "address": {\n "description": "A provider-normalized procedure address",\n "type": "string"\n },\n "direction": {\n "default": "forward",\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "forward",\n "backward"\n ],\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "get_call_graph",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n },\n "__schema1": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema1"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema1"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema1"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "calls": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "ok",\n "type": "string"\n }\n },\n "required": [\n "address",\n "status",\n "calls"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "error": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "invalid_request",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "unreadable_output",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "capability_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "provider_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "provider_timeout",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "cancelled",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "artifact_integrity_mismatch",\n "type": "string"\n },\n "details": {\n "additionalProperties": false,\n "properties": {\n "calculated_sha256": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declared_sha256": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "logical_path": {\n "type": "string"\n },\n "unpacked": {\n "type": "boolean"\n }\n },\n "required": [\n "logical_path",\n "declared_sha256",\n "calculated_sha256",\n "unpacked"\n ],\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation",\n "details"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "artifact_operation_failed",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "evidence_integrity_mismatch",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "truncated",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "process_capture_failed",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "cleanup_incomplete",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "revision_conflict",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "configuration_invalid",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "target_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "execution_failure",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "status": {\n "const": "error",\n "type": "string"\n }\n },\n "required": [\n "address",\n "status",\n "error"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Get Call Graph"\n },\n {\n "analysisOperation": "analyze_swift_types",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Categorize analyzed procedure names into Swift classes, structs, enums, protocols, extensions, and other symbols. Returns deduplicated names grouped with counts. Optionally select one category and/or apply a case-sensitive literal name filter; for example, use {category: \\"classes\\", pattern: \\"Account\\"} to find matching class symbols.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "category": "classes",\n "pattern": "Account"\n }\n ],\n "properties": {\n "category": {\n "description": "Limit results to one Swift symbol category.",\n "enum": [\n "classes",\n "structs",\n "enums",\n "protocols",\n "extensions",\n "other"\n ],\n "type": "string"\n },\n "pattern": {\n "description": "Case-sensitive literal filter applied to mangled symbol names.",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "enhanced",\n "name": "analyze_swift_types",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "categories": {\n "additionalProperties": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "count",\n "items"\n ],\n "type": "object"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "categories"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Analyze Swift Types"\n },\n {\n "analysisOperation": "find_xrefs_to_name",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Resolve an exact name against the bound provider\'s name inventory and return a resolved or unresolved result. Unresolved names use the stable name_not_found reason; this xref workflow returns address-only projections.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "name": "malloc"\n }\n ],\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n }\n },\n "required": [\n "name"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "find_xrefs_to_name",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "status": {\n "const": "resolved",\n "type": "string"\n },\n "xrefs": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "name",\n "address",\n "xrefs"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "reason": {\n "const": "name_not_found",\n "type": "string"\n },\n "status": {\n "const": "unresolved",\n "type": "string"\n }\n },\n "required": [\n "status",\n "name",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Find Xrefs To Name"\n },\n {\n "analysisOperation": "binary_overview",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return native-binary metadata, every segment with its length, and exhaustive procedure/string counts. Use search_procedures, list_procedures, or analyze_function directly when you already know which procedure to inspect.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "enhanced",\n "name": "binary_overview",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "document": {\n "type": "string"\n },\n "procedure_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "segment_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "segments": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "type": "string"\n },\n "length": {\n "minimum": 0,\n "type": "number"\n },\n "name": {\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "start",\n "end",\n "length"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "string_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "document",\n "segments",\n "segment_count",\n "procedure_count",\n "string_count"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Binary Overview"\n },\n {\n "analysisOperation": "analyze_function",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Build a dossier for one native function identified by symbol or provider-returned address. Returns identity with complete inclusive body ranges or explicit unknown, provider-specific pseudocode and assembly, comments, calls, references, referenced strings/names, local CFG blocks, and available native API boundary observations.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "procedure": {\n "description": "A procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "analyze_function",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "assembly": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "basic_blocks": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "type": "string"\n },\n "start": {\n "type": "string"\n },\n "successors": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "start",\n "end",\n "successors"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "callees": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "callers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "comments": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "comment",\n "inline"\n ],\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "kind",\n "text"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "incoming_references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "conditional": {\n "type": "boolean"\n },\n "data": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "flow": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "primary": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "read": {\n "type": "boolean"\n },\n "terminal": {\n "type": "boolean"\n },\n "type": {\n "type": "string"\n },\n "write": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "type",\n "flow",\n "call",\n "jump",\n "data",\n "read",\n "write",\n "indirect",\n "computed",\n "conditional",\n "terminal",\n "primary",\n "operand_index",\n "external"\n ],\n "type": "object"\n }\n ]\n },\n "source_address": {\n "type": "string"\n },\n "source_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_address": {\n "type": "string"\n },\n "target_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "source_address",\n "target_address",\n "source_procedure",\n "target_procedure",\n "kind"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "default": [],\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "native_api": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "residual_unknowns": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "available",\n "reason",\n "residual_unknowns"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "calling_convention": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-and-stack-variables",\n "compiler-generated-control-flow",\n "pointer-arithmetic",\n "pseudocode"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "jump_tables": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "data_sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "entry_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "entry_size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "provenance": {\n "enum": [\n "ghidra-decompiler-load-table",\n "ghidra-dispatch-block-data-reference",\n "ghidra-aarch64-byte-table",\n "ghidra-aarch64-halfword-table"\n ],\n "type": "string"\n }\n },\n "required": [\n "address",\n "provenance",\n "entry_size_bytes",\n "entry_count",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "default_targets": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "target_address": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_address",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "dispatch_address": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "case_value": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "target_address": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "case_value",\n "target_address",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "dispatch_address",\n "data_sources",\n "mappings",\n "default_targets",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "parameters": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "data_type": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-or-stack-storage",\n "compiler-generated-variable"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "role": {\n "const": "parameter",\n "type": "string"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type",\n "size_bytes",\n "storage",\n "confidence",\n "evidence",\n "decompiler_artifacts",\n "role"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "pseudocode": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "const": "decompiler-generated-non-source",\n "type": "string"\n },\n "compilable": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "classification",\n "compilable"\n ],\n "type": "object"\n },\n "return_type": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "data_type": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-or-stack-storage",\n "compiler-generated-variable"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "null"\n },\n "ordinal": {\n "type": "null"\n },\n "role": {\n "const": "return",\n "type": "string"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type",\n "size_bytes",\n "storage",\n "confidence",\n "evidence",\n "decompiler_artifacts",\n "role"\n ],\n "type": "object"\n },\n "signature_source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "available",\n "provenance",\n "signature_source",\n "calling_convention",\n "return_type",\n "parameters",\n "jump_tables",\n "pseudocode",\n "decompiler_artifacts",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "native_value_flow": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "def_use": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "definition": {\n "minLength": 1,\n "type": "string"\n },\n "input_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "use": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "definition",\n "use",\n "input_index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "effects": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "memory_read",\n "memory_write",\n "conditional_branch",\n "indirect_branch",\n "direct_call",\n "indirect_call"\n ],\n "type": "string"\n },\n "operand_input": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "value_input": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "operation",\n "kind",\n "operand_input",\n "value_input"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "known_omitted_edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "known_omitted_inputs": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "omitted_operations_lower_bound": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "operations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "block_membership": {\n "default": "unavailable",\n "enum": [\n "member",\n "detached",\n "unavailable"\n ],\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "inputs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "constant_hex": {\n "anyOf": [\n {\n "pattern": "^[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "constant",\n "address",\n "register",\n "unique",\n "other"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "size_bytes",\n "location",\n "constant_hex"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "is_dead": {\n "type": "boolean"\n },\n "opcode": {\n "minLength": 1,\n "type": "string"\n },\n "output": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "constant_hex": {\n "anyOf": [\n {\n "pattern": "^[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "constant",\n "address",\n "register",\n "unique",\n "other"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "size_bytes",\n "location",\n "constant_hex"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "id",\n "address",\n "sequence",\n "opcode",\n "is_dead",\n "block_membership",\n "inputs",\n "output"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "parameter_uses": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "input_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "use": {\n "type": "string"\n }\n },\n "required": [\n "ordinal",\n "use",\n "input_index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "parameters": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "data_type": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provenance": {\n "const": "ghidra-high-pcode",\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "operations",\n "def_use",\n "effects",\n "parameters",\n "parameter_uses",\n "truncated",\n "omitted_operations_lower_bound",\n "known_omitted_inputs",\n "known_omitted_edges",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "outgoing_references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "call": {\n "type": "boolean"\n },\n "computed": {\n "type": "boolean"\n },\n "conditional": {\n "type": "boolean"\n },\n "data": {\n "type": "boolean"\n },\n "external": {\n "type": "boolean"\n },\n "flow": {\n "type": "boolean"\n },\n "indirect": {\n "type": "boolean"\n },\n "jump": {\n "type": "boolean"\n },\n "operand_index": {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n "primary": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "read": {\n "type": "boolean"\n },\n "terminal": {\n "type": "boolean"\n },\n "type": {\n "type": "string"\n },\n "write": {\n "type": "boolean"\n }\n },\n "required": [\n "available",\n "provenance",\n "type",\n "flow",\n "call",\n "jump",\n "data",\n "read",\n "write",\n "indirect",\n "computed",\n "conditional",\n "terminal",\n "primary",\n "operand_index",\n "external"\n ],\n "type": "object"\n }\n ]\n },\n "source_address": {\n "type": "string"\n },\n "source_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_address": {\n "type": "string"\n },\n "target_procedure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "source_address",\n "target_address",\n "source_procedure",\n "target_procedure",\n "kind"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "locals": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "description": {\n "type": "string"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "description",\n "provenance"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "signature": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body",\n "signature",\n "locals"\n ],\n "type": "object"\n },\n "pseudocode": {\n "type": "string"\n },\n "referenced_names": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "source_address": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value",\n "source_address"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "referenced_strings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "source_address": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "value",\n "source_address"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "procedure",\n "pseudocode",\n "assembly",\n "comments",\n "callers",\n "callees",\n "incoming_references",\n "outgoing_references",\n "referenced_strings",\n "referenced_names",\n "basic_blocks",\n "native_api",\n "native_value_flow",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Analyze Function"\n },\n {\n "analysisOperation": "inspect_native_api",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Analyze a native API boundary in one function identified by symbol or provider-returned address. Returns confidence, evidence, jump-table dispatch/data/case mappings, separately evidenced default targets, unsupported branches, and residual unknowns. Null case values mean unresolved cases, never a known default.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "procedure": "main"\n }\n ],\n "properties": {\n "procedure": {\n "description": "A procedure name or address",\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "inspect_native_api",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "boundary": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "residual_unknowns": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "available",\n "reason",\n "residual_unknowns"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "calling_convention": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-and-stack-variables",\n "compiler-generated-control-flow",\n "pointer-arithmetic",\n "pseudocode"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "jump_tables": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "data_sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "entry_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "entry_size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "provenance": {\n "enum": [\n "ghidra-decompiler-load-table",\n "ghidra-dispatch-block-data-reference",\n "ghidra-aarch64-byte-table",\n "ghidra-aarch64-halfword-table"\n ],\n "type": "string"\n }\n },\n "required": [\n "address",\n "provenance",\n "entry_size_bytes",\n "entry_count",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "default_targets": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "target_address": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_address",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "dispatch_address": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "case_value": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "target_address": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "case_value",\n "target_address",\n "confidence",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "dispatch_address",\n "data_sources",\n "mappings",\n "default_targets",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "parameters": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "data_type": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-or-stack-storage",\n "compiler-generated-variable"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "role": {\n "const": "parameter",\n "type": "string"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type",\n "size_bytes",\n "storage",\n "confidence",\n "evidence",\n "decompiler_artifacts",\n "role"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "pseudocode": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "const": "decompiler-generated-non-source",\n "type": "string"\n },\n "compilable": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "classification",\n "compilable"\n ],\n "type": "object"\n },\n "return_type": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "data_type": {\n "minLength": 1,\n "type": "string"\n },\n "decompiler_artifacts": {\n "items": {\n "enum": [\n "recovered-signature",\n "register-or-stack-storage",\n "compiler-generated-variable"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "signature-source",\n "decompiler-type",\n "type-lock",\n "calling-convention",\n "jump-table"\n ],\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "source",\n "detail"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "null"\n },\n "ordinal": {\n "type": "null"\n },\n "role": {\n "const": "return",\n "type": "string"\n },\n "size_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type",\n "size_bytes",\n "storage",\n "confidence",\n "evidence",\n "decompiler_artifacts",\n "role"\n ],\n "type": "object"\n },\n "signature_source": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "available",\n "provenance",\n "signature_source",\n "calling_convention",\n "return_type",\n "parameters",\n "jump_tables",\n "pseudocode",\n "decompiler_artifacts",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "residual_unknowns": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "substeps": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "enum": [\n "analyze_function",\n "project_native_api_boundary",\n "preserve_residual_unknowns"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "completed",\n "unsupported"\n ],\n "type": "string"\n }\n },\n "required": [\n "operation",\n "status",\n "observations"\n ],\n "type": "object"\n },\n "maxItems": 3,\n "minItems": 3,\n "type": "array"\n },\n "unsupported_branches": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "procedure",\n "boundary",\n "substeps",\n "unsupported_branches",\n "residual_unknowns"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Native Api"\n },\n {\n "analysisOperation": "trace_feature",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Trace a literal feature query through every matching string and procedure, their xrefs, and truthful containing-procedure resolution. Returns observations, operation count, and residual unknowns without inferring reference kinds.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "case_sensitive": false,\n "query": "license"\n }\n ],\n "properties": {\n "case_sensitive": {\n "default": false,\n "description": "Whether text matching distinguishes uppercase and lowercase.",\n "type": "boolean"\n },\n "query": {\n "description": "Non-empty feature or text query to investigate.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "query"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "trace_feature",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "matches": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "type": {\n "enum": [\n "string",\n "procedure"\n ],\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "type",\n "address",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "query": {\n "type": "string"\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "containing_procedure": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "found": {\n "const": true,\n "type": "boolean"\n },\n "procedure": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "body": {\n "default": {\n "available": false,\n "reason": "The provider did not report complete function body ranges."\n },\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "contains_entry": {\n "type": "boolean"\n },\n "non_contiguous": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "description": "Inclusive last address of this observed body range.",\n "type": "string"\n },\n "start": {\n "type": "string"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "span_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "total_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "available",\n "provenance",\n "ranges",\n "total_bytes",\n "span_bytes",\n "non_contiguous",\n "contains_entry"\n ],\n "type": "object"\n }\n ]\n },\n "classification": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "external": {\n "type": "boolean"\n },\n "provenance": {\n "minLength": 1,\n "type": "string"\n },\n "thunk": {\n "type": "boolean"\n },\n "thunk_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "external",\n "thunk",\n "thunk_target",\n "provenance"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name",\n "classification",\n "body"\n ],\n "type": "object"\n },\n "query_address": {\n "type": "string"\n }\n },\n "required": [\n "query_address",\n "found",\n "procedure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "found": {\n "const": false,\n "type": "boolean"\n },\n "procedure": {\n "type": "null"\n },\n "query_address": {\n "type": "string"\n },\n "reason": {\n "enum": [\n "outside_segments",\n "not_in_procedure"\n ],\n "type": "string"\n }\n },\n "required": [\n "query_address",\n "found",\n "procedure",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "source_address": {\n "type": "string"\n },\n "target_address": {\n "type": "string"\n }\n },\n "required": [\n "target_address",\n "source_address",\n "containing_procedure"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "residual_unknowns": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "search_mode": {\n "const": "literal",\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "query",\n "search_mode",\n "matches",\n "references",\n "truncated",\n "residual_unknowns"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Feature"\n },\n {\n "analysisOperation": "trace_call_path",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Trace direct callers or callees from one exact procedure address until the graph is exhausted or the optional goal is reached. Returns visited nodes, direct-call edges, a shortest traversal path, provider failures, and residual unknowns; unresolved indirect calls remain unknown.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "direction": "forward",\n "goal": "0x1100",\n "start": "0x1000"\n }\n ],\n "properties": {\n "direction": {\n "default": "forward",\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "forward",\n "backward"\n ],\n "type": "string"\n },\n "goal": {\n "description": "An optional provider-normalized destination address",\n "type": "string"\n },\n "start": {\n "description": "A provider-normalized procedure address",\n "type": "string"\n }\n },\n "required": [\n "start"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "trace_call_path",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n },\n "__schema1": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema1"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema1"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema1"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "direction": {\n "enum": [\n "forward",\n "backward"\n ],\n "type": "string"\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "discovery_depth": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source_address": {\n "type": "string"\n },\n "target_address": {\n "type": "string"\n }\n },\n "required": [\n "source_address",\n "target_address",\n "discovery_depth"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "failures": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "error": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "invalid_request",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "unreadable_output",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "capability_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "provider_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "provider_timeout",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "cancelled",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "artifact_integrity_mismatch",\n "type": "string"\n },\n "details": {\n "additionalProperties": false,\n "properties": {\n "calculated_sha256": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declared_sha256": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "logical_path": {\n "type": "string"\n },\n "unpacked": {\n "type": "boolean"\n }\n },\n "required": [\n "logical_path",\n "declared_sha256",\n "calculated_sha256",\n "unpacked"\n ],\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation",\n "details"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "artifact_operation_failed",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "evidence_integrity_mismatch",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "truncated",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "process_capture_failed",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "cleanup_incomplete",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "revision_conflict",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "configuration_invalid",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "target_unavailable",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "category": {\n "enum": [\n "invalid_input",\n "unsupported_provider",\n "integrity_mismatch",\n "truncated",\n "cancelled",\n "timeout",\n "unavailable",\n "execution_failure"\n ],\n "type": "string"\n },\n "code": {\n "const": "execution_failure",\n "type": "string"\n },\n "details": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "remediation": {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "retryable": {\n "type": "boolean"\n }\n },\n "required": [\n "code",\n "category",\n "message",\n "retryable",\n "remediation"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "address",\n "error"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "goal": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "goal_status": {\n "enum": [\n "not_requested",\n "reached",\n "not_reached"\n ],\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "depth": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "address",\n "depth"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "residual_unknowns": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "start": {\n "type": "string"\n },\n "traversal": {\n "additionalProperties": false,\n "properties": {\n "nodes_visited": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "nodes_visited"\n ],\n "type": "object"\n },\n "traversal_path": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "start",\n "goal",\n "direction",\n "goal_status",\n "nodes",\n "edges",\n "traversal_path",\n "failures",\n "traversal",\n "truncated",\n "residual_unknowns",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Call Path"\n },\n {\n "analysisOperation": "trace_native_ui_action",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Trace one unique compiled UI action, object ID, native symbol or exact function address through authored connections, encoded or symbolized Objective-C handlers, and bounded static call references. Typed direct and resolved indirect calls, ambiguous candidates, inferred untyped provider callees, and targetless sites remain distinct. Runtime reachability is unknown; use trace_native_values for recovered value dependencies.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "action": "buildTapped:",\n "max_depth": 8,\n "max_edges": 500,\n "max_nodes": 250\n }\n ],\n "properties": {\n "action": {\n "description": "A unique compiled UI action selector, interface object ID, native symbol or exact function address",\n "minLength": 1,\n "type": "string"\n },\n "max_depth": {\n "default": 8,\n "description": "Maximum permitted depth for this operation.",\n "maximum": 32,\n "minimum": 0,\n "type": "integer"\n },\n "max_edges": {\n "default": 500,\n "description": "Maximum permitted edges for this operation.",\n "maximum": 5000,\n "minimum": 1,\n "type": "integer"\n },\n "max_nodes": {\n "default": 250,\n "description": "Maximum permitted nodes for this operation.",\n "maximum": 2000,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "action"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "trace_native_ui_action",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "examined": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "facet": {\n "minLength": 1,\n "type": "string"\n },\n "omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "unsupported",\n "not_requested"\n ],\n "type": "string"\n }\n },\n "required": [\n "facet",\n "status",\n "reason",\n "examined",\n "omitted"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "direction": {\n "enum": [\n "forward",\n "backward"\n ],\n "type": "string"\n },\n "edges": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "from": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "relation": {\n "enum": [\n "contains",\n "outlet_to",\n "target_action",\n "segue_to",\n "objc_dispatch",\n "swift_witness_dispatch",\n "swift_vtable_dispatch",\n "thunk_to",\n "direct_call",\n "indirect_call",\n "provider_call",\n "reads",\n "writes",\n "flows_to",\n "controls"\n ],\n "type": "string"\n },\n "resolution": {\n "enum": [\n "observed",\n "inferred",\n "resolved",\n "ambiguous"\n ],\n "type": "string"\n },\n "to": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "from",\n "to",\n "relation",\n "resolution",\n "evidence",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "from": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "relation": {\n "enum": [\n "contains",\n "outlet_to",\n "target_action",\n "segue_to",\n "objc_dispatch",\n "swift_witness_dispatch",\n "swift_vtable_dispatch",\n "thunk_to",\n "direct_call",\n "indirect_call",\n "provider_call",\n "reads",\n "writes",\n "flows_to",\n "controls"\n ],\n "type": "string"\n },\n "resolution": {\n "const": "unresolved",\n "type": "string"\n },\n "to": {\n "type": "null"\n }\n },\n "required": [\n "id",\n "from",\n "to",\n "relation",\n "resolution",\n "reason",\n "evidence",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attributes": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "storyboard",\n "scene",\n "view_controller",\n "view",\n "control",\n "constraint",\n "layout_guide",\n "resource",\n "placeholder",\n "external_object",\n "outlet",\n "action",\n "segue",\n "objc_class",\n "objc_selector",\n "swift_declaration",\n "swift_requirement",\n "dispatch_table",\n "dispatch_slot",\n "thunk",\n "function",\n "state_value",\n "global_value",\n "field",\n "branch_condition",\n "sink",\n "unknown"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "kind",\n "name",\n "location",\n "attributes",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "version",\n "tool_version"\n ],\n "type": "object"\n },\n "reached_depth": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "start": {\n "minLength": 1,\n "type": "string"\n },\n "target_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n },\n "unresolved": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "from": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "relation": {\n "enum": [\n "contains",\n "outlet_to",\n "target_action",\n "segue_to",\n "objc_dispatch",\n "swift_witness_dispatch",\n "swift_vtable_dispatch",\n "thunk_to",\n "direct_call",\n "indirect_call",\n "provider_call",\n "reads",\n "writes",\n "flows_to",\n "controls"\n ],\n "type": "string"\n },\n "resolution": {\n "enum": [\n "observed",\n "inferred",\n "resolved",\n "ambiguous"\n ],\n "type": "string"\n },\n "to": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "from",\n "to",\n "relation",\n "resolution",\n "evidence",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "from": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "relation": {\n "enum": [\n "contains",\n "outlet_to",\n "target_action",\n "segue_to",\n "objc_dispatch",\n "swift_witness_dispatch",\n "swift_vtable_dispatch",\n "thunk_to",\n "direct_call",\n "indirect_call",\n "provider_call",\n "reads",\n "writes",\n "flows_to",\n "controls"\n ],\n "type": "string"\n },\n "resolution": {\n "const": "unresolved",\n "type": "string"\n },\n "to": {\n "type": "null"\n }\n },\n "required": [\n "id",\n "from",\n "to",\n "relation",\n "resolution",\n "reason",\n "evidence",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "target_sha256",\n "provider",\n "start",\n "direction",\n "nodes",\n "edges",\n "unresolved",\n "reached_depth",\n "truncated",\n "reason",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Native Ui Action"\n },\n {\n "analysisOperation": "trace_native_values",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Trace a bounded static dependency graph from one explicit native procedure. Includes high-p-code def-use, constants, operators, memory and branch operations, resolved call edges, and derived argument/parameter and return/output bindings. Missing bindings, alias semantics, persistent state and RNG roles remain unknown. Budgets bound depth, decompilations, graph size and payloads; nodes are paginated.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "limit": 1000,\n "max_call_sites": 256,\n "max_depth": 3,\n "max_edges": 10000,\n "max_functions": 16,\n "max_nodes": 5000,\n "offset": 0,\n "procedure": "0x1000"\n }\n ],\n "properties": {\n "limit": {\n "default": 1000,\n "description": "Value for limit.",\n "maximum": 5000,\n "minimum": 1,\n "type": "integer"\n },\n "max_call_sites": {\n "default": 256,\n "description": "Maximum permitted call sites for this operation.",\n "maximum": 4096,\n "minimum": 1,\n "type": "integer"\n },\n "max_depth": {\n "default": 3,\n "description": "Maximum permitted depth for this operation.",\n "maximum": 16,\n "minimum": 0,\n "type": "integer"\n },\n "max_edges": {\n "default": 10000,\n "description": "Maximum permitted edges for this operation.",\n "maximum": 40000,\n "minimum": 1,\n "type": "integer"\n },\n "max_functions": {\n "default": 16,\n "description": "Maximum permitted functions for this operation.",\n "maximum": 64,\n "minimum": 1,\n "type": "integer"\n },\n "max_nodes": {\n "default": 5000,\n "description": "Maximum permitted nodes for this operation.",\n "maximum": 20000,\n "minimum": 1,\n "type": "integer"\n },\n "offset": {\n "default": 0,\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "procedure": {\n "description": "Value for procedure.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "procedure"\n ],\n "type": "object"\n },\n "kind": "enhanced",\n "name": "trace_native_values",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "decompilations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "type": "string"\n },\n "input_index": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "def-use",\n "call",\n "parameter-use",\n "argument-binding",\n "return-binding"\n ],\n "type": "string"\n },\n "source": {\n "type": "string"\n },\n "status": {\n "enum": [\n "derived",\n "direct",\n "resolved-indirect",\n "candidate"\n ],\n "type": "string"\n },\n "target": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "target",\n "input_index",\n "kind",\n "status",\n "evidence_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "next_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "type": "string"\n },\n "id": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "operation",\n "parameter"\n ],\n "type": "string"\n },\n "operation": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "block_membership": {\n "default": "unavailable",\n "enum": [\n "member",\n "detached",\n "unavailable"\n ],\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "inputs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "constant_hex": {\n "anyOf": [\n {\n "pattern": "^[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "constant",\n "address",\n "register",\n "unique",\n "other"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "size_bytes",\n "location",\n "constant_hex"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "is_dead": {\n "type": "boolean"\n },\n "opcode": {\n "minLength": 1,\n "type": "string"\n },\n "output": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "constant_hex": {\n "anyOf": [\n {\n "pattern": "^[0-9a-f]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "constant",\n "address",\n "register",\n "unique",\n "other"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "size_bytes",\n "location",\n "constant_hex"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "id",\n "address",\n "sequence",\n "opcode",\n "is_dead",\n "block_membership",\n "inputs",\n "output"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "parameter": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "data_type": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "ordinal": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "ordinal",\n "name",\n "data_type"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "procedure": {\n "type": "string"\n }\n },\n "required": [\n "id",\n "procedure",\n "evidence_id",\n "kind",\n "operation",\n "parameter"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "procedures": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "analysis_profile_digest": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "depth": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "evidence_id": {\n "type": "string"\n },\n "provider": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "depth",\n "evidence_id",\n "provider",\n "analysis_profile_digest"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "seed": {\n "type": "string"\n },\n "target_sha256": {\n "type": "string"\n },\n "total_edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n },\n "unknowns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "procedure": {\n "type": "string"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "procedure",\n "address",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "seed",\n "target_sha256",\n "nodes",\n "edges",\n "procedures",\n "unknowns",\n "total_nodes",\n "total_edges",\n "decompilations",\n "next_offset",\n "truncated",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Native Values"\n },\n {\n "analysisOperation": "observe_native_ui",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Observe an already-running native app by PID and window ID, bound to the active Mach-O target. Captures a bounded accessibility tree and selected-window screenshot; OS permission denial fails without broad capture or target launch.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "pid": 123,\n "window_id": 456\n }\n ],\n "properties": {\n "accessibility": {\n "default": true,\n "description": "Value for accessibility.",\n "type": "boolean"\n },\n "max_nodes": {\n "default": 500,\n "description": "Maximum accessibility nodes per capture; increase for large windows.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "pid": {\n "description": "Value for pid.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "screenshot": {\n "default": true,\n "description": "Value for screenshot.",\n "type": "boolean"\n },\n "window_id": {\n "description": "Value for window id.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id"\n ],\n "type": "object"\n },\n "kind": "native-provider",\n "name": "observe_native_ui",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "initial": {\n "additionalProperties": false,\n "properties": {\n "gaps": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "children_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "title": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "role",\n "title",\n "value",\n "actions",\n "children_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "screenshot": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base64": {\n "minLength": 4,\n "type": "string"\n },\n "height": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "mime_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "width": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "mime_type",\n "base64",\n "sha256",\n "width",\n "height"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "type": "string"\n },\n "launch_time": {\n "type": "number"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "title": {\n "type": "string"\n },\n "window_id": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "executable",\n "launch_time",\n "title"\n ],\n "type": "object"\n }\n },\n "required": [\n "window",\n "nodes",\n "truncated",\n "screenshot",\n "gaps"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "restore": {\n "const": "leave-as-is",\n "type": "string"\n },\n "steps": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "after": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "gaps": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "children_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "title": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "role",\n "title",\n "value",\n "actions",\n "children_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "screenshot": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base64": {\n "minLength": 4,\n "type": "string"\n },\n "height": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "mime_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "width": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "mime_type",\n "base64",\n "sha256",\n "width",\n "height"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "type": "string"\n },\n "launch_time": {\n "type": "number"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "title": {\n "type": "string"\n },\n "window_id": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "executable",\n "launch_time",\n "title"\n ],\n "type": "object"\n }\n },\n "required": [\n "window",\n "nodes",\n "truncated",\n "screenshot",\n "gaps"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "before": {\n "additionalProperties": false,\n "properties": {\n "gaps": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "children_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "title": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "role",\n "title",\n "value",\n "actions",\n "children_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "screenshot": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base64": {\n "minLength": 4,\n "type": "string"\n },\n "height": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "mime_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "width": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "mime_type",\n "base64",\n "sha256",\n "width",\n "height"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "type": "string"\n },\n "launch_time": {\n "type": "number"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "title": {\n "type": "string"\n },\n "window_id": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "executable",\n "launch_time",\n "title"\n ],\n "type": "object"\n }\n },\n "required": [\n "window",\n "nodes",\n "truncated",\n "screenshot",\n "gaps"\n ],\n "type": "object"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "type": "string"\n },\n "outcome": {\n "enum": [\n "completed",\n "failed",\n "cancelled"\n ],\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "index",\n "kind",\n "before",\n "after",\n "outcome",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target_sha256": {\n "type": "string"\n }\n },\n "required": [\n "target_sha256",\n "initial",\n "steps",\n "restore",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Observe Native Ui"\n },\n {\n "analysisOperation": "capture_native_ui_scenario",\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Run selected-element press, increment/decrement scroll, AXValue text entry and bounded wait steps in one exact native app window. Returns ordered before/after captures and action/capture failures. Actions can change app data, network activity and persistent state; application state is left as-is and restoration is not attempted.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": true,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": true,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "pid": 123,\n "steps": [\n {\n "kind": "wait",\n "milliseconds": 100\n }\n ],\n "window_id": 456\n }\n ],\n "properties": {\n "accessibility": {\n "default": true,\n "description": "Value for accessibility.",\n "type": "boolean"\n },\n "max_nodes": {\n "default": 500,\n "description": "Maximum accessibility nodes per capture; increase for large windows.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "pid": {\n "description": "Value for pid.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "screenshot": {\n "default": true,\n "description": "Value for screenshot.",\n "type": "boolean"\n },\n "steps": {\n "description": "Value for steps.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "click",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "maxItems": 32,\n "type": "array"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "direction": {\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "increment",\n "decrement"\n ],\n "type": "string"\n },\n "kind": {\n "const": "scroll",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "maxItems": 32,\n "type": "array"\n }\n },\n "required": [\n "kind",\n "path",\n "direction"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "key-entry",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "maxItems": 32,\n "type": "array"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path",\n "text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "wait",\n "description": "Value for kind.",\n "type": "string"\n },\n "milliseconds": {\n "description": "Wait duration in milliseconds, within the 180-second operation deadline.",\n "maximum": 180000,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "milliseconds"\n ],\n "type": "object"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n },\n "window_id": {\n "description": "Value for window id.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "steps"\n ],\n "type": "object"\n },\n "kind": "native-provider",\n "name": "capture_native_ui_scenario",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "initial": {\n "additionalProperties": false,\n "properties": {\n "gaps": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "children_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "title": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "role",\n "title",\n "value",\n "actions",\n "children_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "screenshot": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base64": {\n "minLength": 4,\n "type": "string"\n },\n "height": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "mime_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "width": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "mime_type",\n "base64",\n "sha256",\n "width",\n "height"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "type": "string"\n },\n "launch_time": {\n "type": "number"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "title": {\n "type": "string"\n },\n "window_id": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "executable",\n "launch_time",\n "title"\n ],\n "type": "object"\n }\n },\n "required": [\n "window",\n "nodes",\n "truncated",\n "screenshot",\n "gaps"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "restore": {\n "const": "leave-as-is",\n "type": "string"\n },\n "steps": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "after": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "gaps": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "children_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "title": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "role",\n "title",\n "value",\n "actions",\n "children_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "screenshot": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base64": {\n "minLength": 4,\n "type": "string"\n },\n "height": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "mime_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "width": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "mime_type",\n "base64",\n "sha256",\n "width",\n "height"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "type": "string"\n },\n "launch_time": {\n "type": "number"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "title": {\n "type": "string"\n },\n "window_id": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "executable",\n "launch_time",\n "title"\n ],\n "type": "object"\n }\n },\n "required": [\n "window",\n "nodes",\n "truncated",\n "screenshot",\n "gaps"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "before": {\n "additionalProperties": false,\n "properties": {\n "gaps": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "children_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "title": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "role",\n "title",\n "value",\n "actions",\n "children_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "screenshot": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base64": {\n "minLength": 4,\n "type": "string"\n },\n "height": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "mime_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "width": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "mime_type",\n "base64",\n "sha256",\n "width",\n "height"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "type": "string"\n },\n "launch_time": {\n "type": "number"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "title": {\n "type": "string"\n },\n "window_id": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "window_id",\n "executable",\n "launch_time",\n "title"\n ],\n "type": "object"\n }\n },\n "required": [\n "window",\n "nodes",\n "truncated",\n "screenshot",\n "gaps"\n ],\n "type": "object"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "type": "string"\n },\n "outcome": {\n "enum": [\n "completed",\n "failed",\n "cancelled"\n ],\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "index",\n "kind",\n "before",\n "after",\n "outcome",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target_sha256": {\n "type": "string"\n }\n },\n "required": [\n "target_sha256",\n "initial",\n "steps",\n "restore",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Capture Native Ui Scenario"\n },\n {\n "analysisOperation": "inspect_macho",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect Mach-O slices, load commands, imports, exports, dependencies, build metadata, segments, sections, permissions, and exact command provenance without launching Hopper.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "native-provider",\n "name": "inspect_macho",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "architectures": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "alignment": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "cpu_subtype": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "cpu_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "cpu_type",\n "cpu_subtype",\n "file_offset",\n "size",\n "alignment"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "build_metadata": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "minimum_os": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "platform": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sdk": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "tools": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "platform",\n "minimum_os",\n "sdk",\n "tools"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "dependencies": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "compatibility_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "current_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "type": "string"\n },\n "path": {\n "type": "string"\n }\n },\n "required": [\n "path",\n "kind",\n "current_version",\n "compatibility_version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "endian": {\n "anyOf": [\n {\n "enum": [\n "little",\n "big"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "entrypoints": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "file_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "file_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "exports": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "pattern": "^0x[a-fA-F0-9]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "reexport": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "source": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "weak": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "address",\n "weak",\n "reexport",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "file_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "format": {\n "const": "mach-o",\n "type": "string"\n },\n "imports": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "pattern": "^0x[a-fA-F0-9]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "reexport": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "source": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "weak": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "address",\n "weak",\n "reexport",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "load_commands": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "fields": {\n "additionalProperties": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "type": "string"\n }\n },\n "required": [\n "index",\n "kind",\n "fields"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "provenance": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "command": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "exit": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "signal": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "signal"\n ],\n "type": "object"\n },\n "stderr_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "stdout_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "tool": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "tool",\n "command",\n "tool_version",\n "version_reason",\n "executable_sha256",\n "exit",\n "stdout_bytes",\n "stderr_bytes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "segments": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "initial_permissions": {\n "additionalProperties": false,\n "properties": {\n "execute": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "raw": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "read": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "write": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "read",\n "write",\n "execute",\n "raw"\n ],\n "type": "object"\n },\n "maximum_permissions": {\n "additionalProperties": false,\n "properties": {\n "execute": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "raw": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "read": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "write": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "read",\n "write",\n "execute",\n "raw"\n ],\n "type": "object"\n },\n "name": {\n "type": "string"\n },\n "sections": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "pattern": "^0x[a-fA-F0-9]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "alignment": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "segment": {\n "type": "string"\n },\n "size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "segment",\n "name",\n "address",\n "size",\n "file_offset",\n "alignment",\n "flags"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "vm_address": {\n "anyOf": [\n {\n "pattern": "^0x[a-fA-F0-9]+$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "vm_size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "vm_address",\n "vm_size",\n "file_offset",\n "file_size",\n "maximum_permissions",\n "initial_permissions",\n "sections"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "uuid": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "word_size": {\n "anyOf": [\n {\n "anyOf": [\n {\n "const": 32,\n "type": "number"\n },\n {\n "const": 64,\n "type": "number"\n }\n ]\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "format",\n "endian",\n "word_size",\n "file_type",\n "flags",\n "uuid",\n "entrypoints",\n "architectures",\n "build_metadata",\n "load_commands",\n "dependencies",\n "imports",\n "exports",\n "segments",\n "provenance",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Macho"\n },\n {\n "analysisOperation": "inspect_signature",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect the active artifact\'s code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state, and exact command provenance.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "native-provider",\n "name": "inspect_signature",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "authorities": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "cdhashes": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "designated_requirement": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "entitlements": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "format": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "hardened_runtime": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "hash_algorithms": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "identifier": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "provenance": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "command": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "exit": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "signal": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "signal"\n ],\n "type": "object"\n },\n "stderr_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "stdout_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "tool": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "tool",\n "command",\n "tool_version",\n "version_reason",\n "executable_sha256",\n "exit",\n "stdout_bytes",\n "stderr_bytes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "signed": {\n "type": "boolean"\n },\n "team_identifier": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "timestamp": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "signed",\n "identifier",\n "team_identifier",\n "format",\n "cdhashes",\n "hash_algorithms",\n "authorities",\n "designated_requirement",\n "entitlements",\n "timestamp",\n "hardened_runtime",\n "provenance",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Signature"\n },\n {\n "analysisOperation": "inspect_plist",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Parse Info.plist from the active artifact by default, or pass any local plist path. Returns normalized JSON rather than plutil text.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "path": "/Applications/Example.app/Contents/Info.plist"\n }\n ],\n "properties": {\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "native-provider",\n "name": "inspect_plist",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "bundle": {\n "additionalProperties": false,\n "properties": {\n "executable": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "identifier": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "short_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "identifier",\n "executable",\n "name",\n "version",\n "short_version"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "xml",\n "binary",\n "json",\n "unknown"\n ],\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "provenance": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "command": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "exit": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "signal": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "signal"\n ],\n "type": "object"\n },\n "stderr_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "stdout_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "tool": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "tool",\n "command",\n "tool_version",\n "version_reason",\n "executable_sha256",\n "exit",\n "stdout_bytes",\n "stderr_bytes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_path": {\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "format",\n "value",\n "bundle",\n "source_path",\n "provenance",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Plist"\n },\n {\n "analysisOperation": "list_architectures",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "List thin or universal Mach-O slices with offsets, sizes, alignment, explicit coverage, and native-tool provenance.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "native-provider",\n "name": "list_architectures",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "architectures": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "alignment": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "cpu_subtype": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "cpu_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "cpu_type",\n "cpu_subtype",\n "file_offset",\n "size",\n "alignment"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "total": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "items",\n "total",\n "exhaustive",\n "limitations"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "provenance": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "command": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "exit": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "signal": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "signal"\n ],\n "type": "object"\n },\n "stderr_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "stdout_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "tool": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "tool",\n "command",\n "tool_version",\n "version_reason",\n "executable_sha256",\n "exit",\n "stdout_bytes",\n "stderr_bytes"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "architectures",\n "provenance",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Architectures"\n },\n {\n "analysisOperation": "demangle_swift",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Demangle an ordered list of single-line Swift symbols without requiring Hopper. Each input returns demangled, unchanged, or invalid status.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "symbols": [\n "$s4Test3fooyyF"\n ]\n }\n ],\n "properties": {\n "symbols": {\n "description": "Ordered Swift symbols to demangle.",\n "items": {\n "minLength": 1,\n "pattern": "^[^\\\\n]*$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "symbols"\n ],\n "type": "object"\n },\n "kind": "native-provider",\n "name": "demangle_swift",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "provenance": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "command": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "exit": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "signal": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "signal"\n ],\n "type": "object"\n },\n "stderr_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "stdout_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "tool": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "tool",\n "command",\n "tool_version",\n "version_reason",\n "executable_sha256",\n "exit",\n "stdout_bytes",\n "stderr_bytes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "symbols": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "input": {\n "type": "string"\n },\n "output": {\n "type": "string"\n },\n "status": {\n "enum": [\n "demangled",\n "unchanged",\n "invalid"\n ],\n "type": "string"\n }\n },\n "required": [\n "input",\n "output",\n "status"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "symbols",\n "provenance",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Demangle Swift"\n },\n {\n "analysisOperation": "inspect_artifact",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect an active archive or application package in one call. Returns the complete content-addressed artifact graph, source Evidence, observations, relationships, integrity contradictions, hypotheses, unexplored branches, limitations, and next probes inline. Read-only DMG mounting is used automatically on supported macOS hosts. Integrity mismatches fail by default; record-and-continue keeps mismatches explicitly untrusted. It does not extract files; use extract_artifact to materialize its regular contents.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {\n "integrity_policy": {\n "default": "fail",\n "description": "Value for integrity policy.",\n "enum": [\n "fail",\n "record-and-continue"\n ],\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "artifact-provider",\n "name": "inspect_artifact",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "contradictions": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "contradiction_id": {\n "pattern": "^aic_[a-f0-9]{64}$",\n "type": "string"\n },\n "declared_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "observed_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "occurrence_id": {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n "statement": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "contradiction_id",\n "statement",\n "occurrence_id",\n "declared_sha256",\n "observed_sha256",\n "evidence_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "hypotheses_omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hypotheses_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "next_probes_omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "next_probes_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "observations_omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "observations_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "relationships_omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "relationships_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "complete-within-substeps",\n "partial",\n "truncated"\n ],\n "type": "string"\n },\n "substeps_completed": {\n "const": 1,\n "type": "number"\n },\n "unexplored_branches_omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unexplored_branches_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "substeps_completed",\n "observations_retained",\n "observations_omitted",\n "relationships_retained",\n "relationships_omitted",\n "hypotheses_retained",\n "hypotheses_omitted",\n "unexplored_branches_retained",\n "unexplored_branches_omitted",\n "next_probes_retained",\n "next_probes_omitted"\n ],\n "type": "object"\n },\n "derived_relationships": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "logical_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "occurrence_id": {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n "relation": {\n "enum": [\n "contains",\n "extracts",\n "slice-of",\n "embeds",\n "loads",\n "maps-source",\n "derived-from"\n ],\n "type": "string"\n },\n "relationship_id": {\n "pattern": "^air_[a-f0-9]{64}$",\n "type": "string"\n },\n "source_artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "relationship_id",\n "relation",\n "source_artifact_id",\n "target_artifact_id",\n "occurrence_id",\n "logical_path",\n "evidence_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "hypotheses": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "basis_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "confidence": {\n "enum": [\n "medium",\n "low"\n ],\n "type": "string"\n },\n "hypothesis_id": {\n "pattern": "^aih_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitation": {\n "minLength": 1,\n "type": "string"\n },\n "statement": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "hypothesis_id",\n "statement",\n "confidence",\n "basis_evidence_ids",\n "limitation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "inspection_id": {\n "pattern": "^ai_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "next_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "enum": [\n "artifact-bytes",\n "managed-static-analysis",\n "native-analysis-provider",\n "ast-static-analysis"\n ],\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale",\n "authority"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "observations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "enum": [\n "root-manifest",\n "artifact",\n "occurrence",\n "integrity"\n ],\n "type": "string"\n },\n "observation_id": {\n "pattern": "^aio_[a-f0-9]{64}$",\n "type": "string"\n },\n "subject": {\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "observation_id",\n "kind",\n "subject",\n "value",\n "evidence_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "subject": {\n "additionalProperties": false,\n "properties": {\n "manifest_id": {\n "pattern": "^agm_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_format": {\n "minLength": 1,\n "type": "string"\n },\n "root_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "manifest_id",\n "root_artifact_id",\n "root_sha256",\n "root_format"\n ],\n "type": "object"\n },\n "substeps": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "operation": {\n "const": "inventory_artifact",\n "type": "string"\n },\n "status": {\n "const": "completed",\n "type": "string"\n },\n "substep_id": {\n "pattern": "^ais_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "substep_id",\n "operation",\n "status",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "unexplored_branches": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "branch_id": {\n "pattern": "^aib_[a-f0-9]{64}$",\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "next_probe": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "enum": [\n "artifact-bytes",\n "managed-static-analysis",\n "native-analysis-provider",\n "ast-static-analysis"\n ],\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale",\n "authority"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "format-specific-analysis-required",\n "unknown-format"\n ],\n "type": "string"\n }\n },\n "required": [\n "branch_id",\n "reason",\n "detail",\n "next_probe",\n "evidence_id"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "inspection_id",\n "subject",\n "substeps",\n "observations",\n "derived_relationships",\n "hypotheses",\n "contradictions",\n "unexplored_branches",\n "next_probes",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Artifact"\n },\n {\n "analysisOperation": "extract_artifact",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Extract all regular files from the active archive or application package into a fresh temporary directory chosen by REA. The result includes its location. Rejects traversal and symlink escapes, never overwrites, enforces archive integrity checks, and verifies cleanup. Integrity mismatches fail by default; record-and-continue extracts the observed bytes and reports each mismatch as an untrusted integrity contradiction.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {\n "integrity_policy": {\n "default": "fail",\n "description": "Value for integrity policy.",\n "enum": [\n "fail",\n "record-and-continue"\n ],\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "artifact-provider",\n "name": "extract_artifact",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "artifacts": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "bytes_written": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "created": {\n "type": "boolean"\n },\n "relative_path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "artifact_id",\n "relative_path",\n "sha256",\n "bytes_written",\n "created"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "cleanup": {\n "additionalProperties": false,\n "properties": {\n "attempted": {\n "type": "boolean"\n },\n "residual_paths": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "verified": {\n "type": "boolean"\n }\n },\n "required": [\n "attempted",\n "verified",\n "residual_paths"\n ],\n "type": "object"\n },\n "containment_verified": {\n "type": "boolean"\n },\n "extraction_manifest": {\n "additionalProperties": false,\n "properties": {\n "extraction_id": {\n "pattern": "^aex_[a-f0-9]{64}$",\n "type": "string"\n },\n "files_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "output_root_alias": {\n "const": "$OUTPUT_ROOT",\n "type": "string"\n },\n "selected_occurrence_ids": {\n "items": {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "source_manifest_id": {\n "pattern": "^agm_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "extraction_id",\n "source_manifest_id",\n "selected_occurrence_ids",\n "files_sha256",\n "output_root_alias"\n ],\n "type": "object"\n },\n "integrity_contradictions": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "contradiction_id": {\n "pattern": "^ic_[a-f0-9]{64}$",\n "type": "string"\n },\n "declared_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "entry_kind": {\n "enum": [\n "file",\n "slice"\n ],\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "logical_path": {\n "minLength": 1,\n "type": "string"\n },\n "observed_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "occurrence_id": {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n "parent_artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "provenance": {\n "const": "container-integrity-metadata-versus-observed-bytes",\n "type": "string"\n },\n "trust": {\n "const": "observed-untrusted",\n "type": "string"\n },\n "unpacked": {\n "type": "boolean"\n }\n },\n "required": [\n "contradiction_id",\n "occurrence_id",\n "parent_artifact_id",\n "logical_path",\n "declared_sha256",\n "observed_sha256",\n "entry_kind",\n "unpacked",\n "trust",\n "provenance",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "edge_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "graph_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "manifest_id": {\n "pattern": "^agm_[a-f0-9]{64}$",\n "type": "string"\n },\n "node_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "occurrence_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "root_artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_format": {\n "enum": [\n "dmg",\n "zip",\n "pkg",\n "asar",\n "javascript-bundle",\n "source-map",\n "mach-o-universal",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "hopper",\n "analysis-database",\n "ipa",\n "apk",\n "msix",\n "appx",\n "plist",\n "entitlements",\n "directory",\n "file",\n "unknown"\n ],\n "type": "string"\n },\n "root_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "manifest_id",\n "root_artifact_id",\n "root_sha256",\n "root_format",\n "graph_sha256",\n "node_count",\n "occurrence_count",\n "edge_count"\n ],\n "type": "object"\n },\n "output_root": {\n "minLength": 1,\n "type": "string"\n },\n "provenance": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "arguments": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "effects": {\n "items": {\n "enum": [\n "read",\n "write",\n "mount"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "exit_code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "tool": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "tool",\n "arguments",\n "tool_version",\n "executable_sha256",\n "exit_code",\n "effects"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "manifest",\n "extraction_manifest",\n "output_root",\n "artifacts",\n "containment_verified",\n "cleanup",\n "provenance",\n "integrity_contradictions",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Extract Artifact"\n },\n {\n "analysisOperation": "decode_interface_builder",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Decode compiled storyboard and nib archives inside the active Apple app bundle, including keyed property lists and NIBArchive object tables, into a bounded object, hierarchy, and connection graph. This is static archive parsing; unrecognized archive classes and unresolved code handlers remain explicit unknowns.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {\n "max_connections": {\n "default": 40000,\n "description": "Maximum permitted connections for this operation.",\n "maximum": 40000,\n "minimum": 1,\n "type": "integer"\n },\n "max_documents": {\n "default": 64,\n "description": "Maximum permitted documents for this operation.",\n "maximum": 64,\n "minimum": 1,\n "type": "integer"\n },\n "max_objects": {\n "default": 20000,\n "description": "Maximum permitted objects for this operation.",\n "maximum": 20000,\n "minimum": 1,\n "type": "integer"\n }\n },\n "type": "object"\n },\n "kind": "artifact-provider",\n "name": "decode_interface_builder",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "documents": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "archive_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "connection_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "document_kind": {\n "enum": [\n "nib",\n "storyboard_scene"\n ],\n "type": "string"\n },\n "hierarchy_complete": {\n "type": "boolean"\n },\n "object_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "relative_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "relative_path",\n "archive_sha256",\n "document_kind",\n "object_count",\n "connection_count",\n "hierarchy_complete"\n ],\n "type": "object"\n },\n "maxItems": 64,\n "type": "array"\n },\n "graph": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "examined": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "facet": {\n "minLength": 1,\n "type": "string"\n },\n "omitted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "unsupported",\n "not_requested"\n ],\n "type": "string"\n }\n },\n "required": [\n "facet",\n "status",\n "reason",\n "examined",\n "omitted"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "edges": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "from": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "relation": {\n "enum": [\n "contains",\n "outlet_to",\n "target_action",\n "segue_to",\n "objc_dispatch",\n "swift_witness_dispatch",\n "swift_vtable_dispatch",\n "thunk_to",\n "direct_call",\n "indirect_call",\n "provider_call",\n "reads",\n "writes",\n "flows_to",\n "controls"\n ],\n "type": "string"\n },\n "resolution": {\n "enum": [\n "observed",\n "inferred",\n "resolved",\n "ambiguous"\n ],\n "type": "string"\n },\n "to": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "from",\n "to",\n "relation",\n "resolution",\n "evidence",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "from": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "relation": {\n "enum": [\n "contains",\n "outlet_to",\n "target_action",\n "segue_to",\n "objc_dispatch",\n "swift_witness_dispatch",\n "swift_vtable_dispatch",\n "thunk_to",\n "direct_call",\n "indirect_call",\n "provider_call",\n "reads",\n "writes",\n "flows_to",\n "controls"\n ],\n "type": "string"\n },\n "resolution": {\n "const": "unresolved",\n "type": "string"\n },\n "to": {\n "type": "null"\n }\n },\n "required": [\n "id",\n "from",\n "to",\n "relation",\n "resolution",\n "reason",\n "evidence",\n "limitations"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attributes": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "description": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "binary_metadata",\n "interface_builder_resource",\n "symbol",\n "reference",\n "decompiler"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "description",\n "location",\n "artifact_path",\n "artifact_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "enum": [\n "storyboard",\n "scene",\n "view_controller",\n "view",\n "control",\n "constraint",\n "layout_guide",\n "resource",\n "placeholder",\n "external_object",\n "outlet",\n "action",\n "segue",\n "objc_class",\n "objc_selector",\n "swift_declaration",\n "swift_requirement",\n "dispatch_table",\n "dispatch_slot",\n "thunk",\n "function",\n "state_value",\n "global_value",\n "field",\n "branch_condition",\n "sink",\n "unknown"\n ],\n "type": "string"\n },\n "location": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "file_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "kind",\n "name",\n "location",\n "attributes",\n "evidence"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "tool_version": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "version",\n "tool_version"\n ],\n "type": "object"\n },\n "target_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "target_sha256",\n "provider",\n "nodes",\n "edges",\n "coverage",\n "truncated"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "target_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "target_sha256",\n "documents",\n "graph",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Decode Interface Builder"\n },\n {\n "analysisOperation": "inspect_keyed_archive",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect one Foundation NSKeyedArchiver plist in the active app bundle as original object-table nodes, class descriptors, named roots, and UID edges. Preserves shared and cyclic references without instantiating classes. The path is relative to the bundle; root selection and object pagination preserve original identities.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "path": "Contents/Resources/Model.plist"\n }\n ],\n "properties": {\n "limit": {\n "default": 20000,\n "description": "Value for limit.",\n "maximum": 20000,\n "minimum": 1,\n "type": "integer"\n },\n "offset": {\n "default": 0,\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "default": ".",\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "root": {\n "description": "Value for root.",\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "artifact-provider",\n "name": "inspect_keyed_archive",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "archive_format": {\n "enum": [\n "binary-plist",\n "xml-plist"\n ],\n "type": "string"\n },\n "archive_path": {\n "type": "string"\n },\n "archive_sha256": {\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "next_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "objects": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "class_id": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "class_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "enum": [\n "nil",\n "scalar",\n "array",\n "dictionary",\n "class-descriptor",\n "archived-object"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "observed",\n "unknown-class",\n "unresolved-class",\n "nil"\n ],\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "id",\n "kind",\n "class_id",\n "class_name",\n "status",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "path": {\n "items": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n ]\n },\n "type": "array"\n },\n "raw": {\n "$ref": "#/$defs/__schema0"\n },\n "source": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "resolved",\n "nil",\n "unresolved",\n "malformed"\n ],\n "type": "string"\n },\n "target": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "source",\n "path",\n "target",\n "status",\n "raw"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "roots": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "target_sha256": {\n "type": "string"\n },\n "total_objects": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total_references": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "target_sha256",\n "archive_path",\n "archive_sha256",\n "archive_format",\n "roots",\n "objects",\n "references",\n "total_objects",\n "total_references",\n "offset",\n "next_offset",\n "truncated",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Keyed Archive"\n },\n {\n "analysisOperation": "inspect_asset_catalog",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect compiled Assets.car metadata in an active Apple app bundle. Returns stable paginated catalog and rendition records with raw assetutil metadata and catalog digests; rendition bytes are not extracted.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "limit": 1000,\n "offset": 0\n }\n ],\n "properties": {\n "limit": {\n "default": 1000,\n "description": "Value for limit.",\n "maximum": 10000,\n "minimum": 1,\n "type": "integer"\n },\n "offset": {\n "default": 0,\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "type": "object"\n },\n "kind": "artifact-provider",\n "name": "inspect_asset_catalog",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "catalogs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "record_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "record_count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limit": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "next_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "records": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "asset_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "catalog_path": {\n "minLength": 1,\n "type": "string"\n },\n "catalog_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "enum": [\n "catalog",\n "rendition"\n ],\n "type": "string"\n },\n "metadata": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "rendition_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "catalog_path",\n "catalog_sha256",\n "index",\n "kind",\n "asset_name",\n "rendition_name",\n "metadata"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "resource_key_matches": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "field": {\n "minLength": 1,\n "type": "string"\n },\n "key": {\n "minLength": 1,\n "type": "string"\n },\n "renditions": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "catalog_path": {\n "minLength": 1,\n "type": "string"\n },\n "catalog_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "record_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "rendition_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "catalog_path",\n "catalog_sha256",\n "record_index",\n "rendition_name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_archive_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_node_id": {\n "minLength": 1,\n "type": "string"\n },\n "source_path": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "enum": [\n "matched",\n "ambiguous",\n "unmatched"\n ],\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "source_path",\n "source_archive_sha256",\n "field",\n "key",\n "status",\n "renditions"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "total_records": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "target_sha256",\n "catalogs",\n "total_records",\n "offset",\n "limit",\n "records",\n "resource_key_matches",\n "next_offset",\n "truncated",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Asset Catalog"\n },\n {\n "analysisOperation": "inspect_managed_artifact",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Open and classify an explicit managed PE/CLI path, or inspect the active managed target, then inventory exact assembly/module identity, target framework evidence, references, resources, and custom attributes without loading or executing target code. Returns complete inline inventories when metadata is admitted and explicit partial or malformed coverage otherwise.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "path": "/tmp/Example.dll"\n }\n ],\n "properties": {\n "path": {\n "description": "Explicit managed PE/CLI path; omit to reuse the previously selected managed target",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "managed-provider",\n "name": "inspect_managed_artifact",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "format": {\n "const": "pe",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "byte_length",\n "format"\n ],\n "type": "object"\n },\n "assembly": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "culture": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "hash_algorithm": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "name": {\n "type": "string"\n },\n "public_key": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "enum": [\n "none",\n "public-key",\n "public-key-token"\n ],\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "token": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{16}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "byte_length",\n "sha256",\n "token"\n ],\n "type": "object"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "version": {\n "pattern": "^\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+$",\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "culture",\n "flags",\n "hash_algorithm",\n "public_key",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "attributes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "constructor_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "decoded_fixed_string": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "parent_token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "type_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "value_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "parent_token",\n "constructor_token",\n "type_name",\n "value_length",\n "value_sha256",\n "decoded_fixed_string",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "classification": {\n "additionalProperties": false,\n "properties": {\n "container": {\n "const": "pe",\n "type": "string"\n },\n "evidence": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "minLength": 1,\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "detail",\n "file_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "implementation": {\n "enum": [\n "cil",\n "cil-and-ready-to-run",\n "cpp-cli-mixed",\n "metadata-only",\n "not-managed",\n "unknown"\n ],\n "type": "string"\n },\n "managed_architecture": {\n "enum": [\n "anycpu",\n "anycpu-prefer-32",\n "x86",\n "x86_64",\n "arm",\n "arm64",\n "unknown"\n ],\n "type": "string"\n },\n "runtime_family": {\n "enum": [\n "dotnet-framework",\n "modern-dotnet",\n "unity-mono",\n "mixed-clr-native",\n "unknown"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "managed",\n "not-managed",\n "malformed"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "container",\n "runtime_family",\n "implementation",\n "managed_architecture",\n "evidence"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "issues": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "invalid-cli-header",\n "invalid-directory",\n "invalid-metadata-root",\n "invalid-stream",\n "missing-stream",\n "invalid-tables",\n "invalid-row",\n "invalid-heap-index",\n "invalid-string",\n "invalid-blob",\n "unsupported-signature",\n "invalid-guid",\n "invalid-resource"\n ],\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "code",\n "scope",\n "offset",\n "detail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "issues"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "metadata": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "stream_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "table_row_counts": {\n "additionalProperties": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "status",\n "version",\n "stream_names",\n "table_row_counts"\n ],\n "type": "object"\n },\n "module": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "enc_base_id": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "enc_id": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "generation": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "name",\n "generation",\n "mvid",\n "enc_id",\n "enc_base_id",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "pe": {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n "characteristics": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "cli": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "entry_point": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "metadata-token",\n "native-rva",\n "none"\n ],\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n "flag_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "flags": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "header_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "header_size": {\n "maximum": 9007199254740991,\n "minimum": 72,\n "type": "integer"\n },\n "managed_native_header_rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "managed_native_header_size": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "metadata_rva": {\n "maximum": 4294967295,\n "minimum": 1,\n "type": "integer"\n },\n "metadata_size": {\n "maximum": 4294967295,\n "minimum": 1,\n "type": "integer"\n },\n "ready_to_run_signature": {\n "type": "boolean"\n },\n "resources_rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "resources_size": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "runtime_version": {\n "type": "string"\n },\n "strong_name_rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "strong_name_size": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "header_offset",\n "header_size",\n "runtime_version",\n "flags",\n "flag_names",\n "entry_point",\n "metadata_rva",\n "metadata_size",\n "resources_rva",\n "resources_size",\n "strong_name_rva",\n "strong_name_size",\n "managed_native_header_rva",\n "managed_native_header_size",\n "ready_to_run_signature"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "machine": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "machine_hex": {\n "pattern": "^0x[0-9a-f]{4}$",\n "type": "string"\n },\n "optional_header": {\n "enum": [\n "pe32",\n "pe32-plus"\n ],\n "type": "string"\n },\n "section_count": {\n "maximum": 65535,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "machine",\n "machine_hex",\n "architecture",\n "optional_header",\n "section_count",\n "characteristics",\n "cli"\n ],\n "type": "object"\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "culture": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "hash_value_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hash_value_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "public_key_or_token": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "enum": [\n "none",\n "public-key",\n "public-key-token"\n ],\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "token": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{16}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "byte_length",\n "sha256",\n "token"\n ],\n "type": "object"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "version": {\n "pattern": "^\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+$",\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "culture",\n "flags",\n "public_key_or_token",\n "hash_value_sha256",\n "hash_value_length",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "resources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "data_length": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "data_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declared_offset": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "embedded": {\n "type": "boolean"\n },\n "flags": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "implementation_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "visibility": {\n "enum": [\n "public",\n "private",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "name",\n "flags",\n "visibility",\n "implementation_token",\n "embedded",\n "declared_offset",\n "data_length",\n "data_sha256",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target_frameworks": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "artifact",\n "pe",\n "classification",\n "metadata",\n "module",\n "assembly",\n "target_frameworks",\n "references",\n "resources",\n "attributes",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Managed Artifact"\n },\n {\n "analysisOperation": "inspect_managed_members",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, and field-access anchors without loading or executing target code. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "path": {\n "description": "Explicit managed PE/CLI path; omit to reuse the previously selected managed target",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "managed-provider",\n "name": "inspect_managed_members",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "format": {\n "const": "pe",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "byte_length",\n "format"\n ],\n "type": "object"\n },\n "call_edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "caller": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "caller_token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "opcode": {\n "type": "string"\n },\n "target_kind": {\n "enum": [\n "method-def",\n "member-ref",\n "method-spec",\n "unknown"\n ],\n "type": "string"\n },\n "target_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "caller_token",\n "caller",\n "opcode",\n "target_token",\n "target_kind",\n "target_name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "issues": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "invalid-cli-header",\n "invalid-directory",\n "invalid-metadata-root",\n "invalid-stream",\n "missing-stream",\n "invalid-tables",\n "invalid-row",\n "invalid-heap-index",\n "invalid-string",\n "invalid-blob",\n "unsupported-signature",\n "invalid-guid",\n "invalid-resource"\n ],\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "code",\n "scope",\n "offset",\n "detail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "issues"\n ],\n "type": "object"\n },\n "field_accesses": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "field_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "field_token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "method": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "method_token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "opcode": {\n "type": "string"\n }\n },\n "required": [\n "method_token",\n "method",\n "opcode",\n "field_token",\n "field_name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "fields": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declaring_type_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "signature": {\n "additionalProperties": false,\n "properties": {\n "calling_convention": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "field_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "generic_parameter_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "issue": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "method",\n "field",\n "unknown"\n ],\n "type": "string"\n },\n "parameter_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "parameter_types": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "parse_status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "malformed"\n ],\n "type": "string"\n },\n "raw_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "raw_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "return_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "raw_length",\n "raw_sha256",\n "kind",\n "parse_status",\n "calling_convention",\n "generic_parameter_count",\n "parameter_count",\n "return_type",\n "parameter_types",\n "field_type",\n "issue"\n ],\n "type": "object"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "declaring_type_token",\n "declaring_type",\n "name",\n "flags",\n "signature"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "identity_scope": {\n "additionalProperties": false,\n "properties": {\n "requires_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "requires_mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "token_identity": {\n "const": "build-local",\n "type": "string"\n }\n },\n "required": [\n "token_identity",\n "requires_artifact_sha256",\n "requires_mvid"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "member_refs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "parent_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "signature": {\n "additionalProperties": false,\n "properties": {\n "calling_convention": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "field_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "generic_parameter_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "issue": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "method",\n "field",\n "unknown"\n ],\n "type": "string"\n },\n "parameter_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "parameter_types": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "parse_status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "malformed"\n ],\n "type": "string"\n },\n "raw_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "raw_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "return_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "raw_length",\n "raw_sha256",\n "kind",\n "parse_status",\n "calling_convention",\n "generic_parameter_count",\n "parameter_count",\n "return_type",\n "parameter_types",\n "field_type",\n "issue"\n ],\n "type": "object"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "parent_token",\n "name",\n "signature"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "metadata": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "table_row_counts": {\n "additionalProperties": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "status",\n "version",\n "table_row_counts"\n ],\n "type": "object"\n },\n "methods": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "body": {\n "additionalProperties": false,\n "properties": {\n "anchors": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "il_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "opcode": {\n "minLength": 1,\n "type": "string"\n },\n "operand": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "operand_kind": {\n "enum": [\n "none",\n "branch",\n "constant",\n "field",\n "method",\n "signature",\n "string",\n "switch",\n "token",\n "type",\n "variable",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "il_offset",\n "opcode",\n "operand_kind",\n "operand"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "decoded_instruction_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exception_regions": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "class_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "filter_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "handler_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "handler_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "try_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "try_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "flags",\n "try_offset",\n "try_length",\n "handler_offset",\n "handler_length",\n "class_token",\n "filter_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "file_offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "header_format": {\n "enum": [\n "tiny",\n "fat",\n "none",\n "unknown"\n ],\n "type": "string"\n },\n "il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "SHA-256 of exactly the raw il_size CIL bytes; excludes the method header, locals signature, alignment, and extra sections"\n },\n "il_size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "init_locals": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "instruction_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "issue": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "local_var_sig_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "max_stack": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Member-result-v1 SHA-256 of UTF-8 JSON [opcode, operand_kind, operand] tuples for a complete decode; retains build-local tokens, omits switch targets, and excludes headers, locals, and exception regions"\n },\n "opcode_counts": {\n "additionalProperties": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "present",\n "partial",\n "absent",\n "malformed"\n ],\n "type": "string"\n },\n "truncated_instructions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "header_format",\n "rva",\n "file_offset",\n "max_stack",\n "init_locals",\n "local_var_sig_token",\n "il_size",\n "il_sha256",\n "normalized_il_sha256",\n "instruction_count",\n "decoded_instruction_count",\n "truncated_instructions",\n "opcode_counts",\n "anchors",\n "exception_regions",\n "issue"\n ],\n "type": "object"\n },\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declaring_type_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "flags": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "impl_flags": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "signature": {\n "additionalProperties": false,\n "properties": {\n "calling_convention": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "field_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "generic_parameter_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "issue": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "method",\n "field",\n "unknown"\n ],\n "type": "string"\n },\n "parameter_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "parameter_types": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "parse_status": {\n "enum": [\n "decoded",\n "partial",\n "unsupported",\n "malformed"\n ],\n "type": "string"\n },\n "raw_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "raw_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "return_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "raw_length",\n "raw_sha256",\n "kind",\n "parse_status",\n "calling_convention",\n "generic_parameter_count",\n "parameter_count",\n "return_type",\n "parameter_types",\n "field_type",\n "issue"\n ],\n "type": "object"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "declaring_type_token",\n "declaring_type",\n "name",\n "rva",\n "impl_flags",\n "flags",\n "signature",\n "body"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "module": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "enc_base_id": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "enc_id": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "generation": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "name",\n "generation",\n "mvid",\n "enc_id",\n "enc_base_id",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "types": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "extends_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "field_list": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "first_row": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "last_row": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "first_row",\n "last_row",\n "count"\n ],\n "type": "object"\n },\n "flags": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "full_name": {\n "type": "string"\n },\n "method_list": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "first_row": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "last_row": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "first_row",\n "last_row",\n "count"\n ],\n "type": "object"\n },\n "name": {\n "type": "string"\n },\n "namespace": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "namespace",\n "name",\n "full_name",\n "flags",\n "extends_token",\n "field_list",\n "method_list"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "artifact",\n "module",\n "metadata",\n "identity_scope",\n "types",\n "fields",\n "methods",\n "member_refs",\n "call_edges",\n "field_accesses",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Managed Members"\n },\n {\n "analysisOperation": "inspect_managed_native_boundaries",\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect PE/CLI ModuleRef, ImplMap/PInvoke declarations, CLI native-header indicators, and non-IL method implementation flags without loading or executing target code. Results are managed declarations and degraded native-boundary observations, not verified native exports or addresses.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "path": {\n "description": "Explicit managed PE/CLI path; omit to reuse the previously selected managed target",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "managed-provider",\n "name": "inspect_managed_native_boundaries",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "format": {\n "const": "pe",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "byte_length",\n "format"\n ],\n "type": "object"\n },\n "cli_native": {\n "additionalProperties": false,\n "properties": {\n "il_only": {\n "type": "boolean"\n },\n "managed_native_header_rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "managed_native_header_size": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "native_entry_point": {\n "type": "boolean"\n },\n "ready_to_run_signature": {\n "type": "boolean"\n },\n "requires_32bit": {\n "type": "boolean"\n },\n "strong_name_signed": {\n "type": "boolean"\n }\n },\n "required": [\n "il_only",\n "requires_32bit",\n "strong_name_signed",\n "native_entry_point",\n "ready_to_run_signature",\n "managed_native_header_rva",\n "managed_native_header_size"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "issues": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "invalid-cli-header",\n "invalid-directory",\n "invalid-metadata-root",\n "invalid-stream",\n "missing-stream",\n "invalid-tables",\n "invalid-row",\n "invalid-heap-index",\n "invalid-string",\n "invalid-blob",\n "unsupported-signature",\n "invalid-guid",\n "invalid-resource"\n ],\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "code",\n "scope",\n "offset",\n "detail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "issues"\n ],\n "type": "object"\n },\n "identity_scope": {\n "additionalProperties": false,\n "properties": {\n "requires_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "requires_mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "token_identity": {\n "const": "build-local",\n "type": "string"\n }\n },\n "required": [\n "token_identity",\n "requires_artifact_sha256",\n "requires_mvid"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "metadata": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "table_row_counts": {\n "additionalProperties": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "status",\n "version",\n "table_row_counts"\n ],\n "type": "object"\n },\n "module": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "enc_base_id": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "enc_id": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "generation": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "name",\n "generation",\n "mvid",\n "enc_id",\n "enc_base_id",\n "token",\n "row_offset"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_refs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "native_implementations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "body_interpretation": {\n "enum": [\n "managed-cil",\n "native-or-runtime",\n "not-file-backed",\n "unknown"\n ],\n "type": "string"\n },\n "boundary_kind": {\n "enum": [\n "pinvoke",\n "native-body",\n "runtime-provided",\n "unmanaged-method",\n "mixed-or-unknown"\n ],\n "type": "string"\n },\n "code_type": {\n "enum": [\n "il",\n "native",\n "optil",\n "runtime",\n "unknown"\n ],\n "type": "string"\n },\n "flags": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "impl_flags": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "managed_kind": {\n "enum": [\n "managed",\n "unmanaged",\n "unknown"\n ],\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "pinvoke_declared": {\n "type": "boolean"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "rva": {\n "maximum": 4294967295,\n "minimum": 0,\n "type": "integer"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "name",\n "rva",\n "flags",\n "impl_flags",\n "code_type",\n "managed_kind",\n "pinvoke_declared",\n "boundary_kind",\n "body_interpretation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "pinvoke_imports": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "best_fit": {\n "enum": [\n "assembly-default",\n "enabled",\n "disabled",\n "unknown"\n ],\n "type": "string"\n },\n "call_convention": {\n "enum": [\n "not-specified",\n "winapi",\n "cdecl",\n "stdcall",\n "thiscall",\n "fastcall",\n "unknown"\n ],\n "type": "string"\n },\n "char_set": {\n "enum": [\n "not-specified",\n "ansi",\n "unicode",\n "auto",\n "unknown"\n ],\n "type": "string"\n },\n "import_name": {\n "type": "string"\n },\n "import_scope_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "import_scope_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "mapping_flags": {\n "maximum": 65535,\n "minimum": 0,\n "type": "integer"\n },\n "mapping_flags_hex": {\n "pattern": "^0x[0-9a-f]{4}$",\n "type": "string"\n },\n "member_kind": {\n "enum": [\n "method",\n "field",\n "unknown"\n ],\n "type": "string"\n },\n "member_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "no_mangle": {\n "type": "boolean"\n },\n "row_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "supports_last_error": {\n "type": "boolean"\n },\n "throw_on_unmappable_char": {\n "enum": [\n "assembly-default",\n "enabled",\n "disabled",\n "unknown"\n ],\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "verification": {\n "const": "managed-declaration-only",\n "type": "string"\n }\n },\n "required": [\n "token",\n "row_offset",\n "mapping_flags",\n "mapping_flags_hex",\n "member_token",\n "member_kind",\n "member_name",\n "import_name",\n "import_scope_token",\n "import_scope_name",\n "no_mangle",\n "char_set",\n "call_convention",\n "supports_last_error",\n "best_fit",\n "throw_on_unmappable_char",\n "verification"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "mixed_mode_or_native_header": {\n "type": "boolean"\n },\n "module_ref_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "native_implementation_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "pinvoke_import_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "ready_to_run": {\n "type": "boolean"\n }\n },\n "required": [\n "module_ref_count",\n "pinvoke_import_count",\n "native_implementation_count",\n "ready_to_run",\n "mixed_mode_or_native_header"\n ],\n "type": "object"\n }\n },\n "required": [\n "artifact",\n "module",\n "metadata",\n "identity_scope",\n "cli_native",\n "module_refs",\n "pinvoke_imports",\n "native_implementations",\n "summary",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Managed Native Boundaries"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect a local APK\'s package, SDK and permission declarations, decoded manifest and class/resource counts. Does not execute the APK or verify signatures. Requires a caller-supplied JADX MCP JAR and JDK; uses one worker in an isolated temporary workspace.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "path": "/tmp/Example.apk"\n }\n ],\n "properties": {\n "path": {\n "description": "Local APK path; the target is never executed",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path"\n ],\n "type": "object"\n },\n "kind": "android-provider",\n "name": "inspect_android_package",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "class_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "heap_limit_mib": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "source_revision": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "worker_count": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "version",\n "artifact_sha256",\n "source_revision",\n "worker_count",\n "heap_limit_mib"\n ],\n "type": "object"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "reported_total_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "status",\n "text",\n "reported_total_bytes"\n ],\n "type": "object"\n },\n "min_sdk": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "package_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "permissions": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "resource_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "signature_verification": {\n "const": "not_performed",\n "type": "string"\n },\n "target_sdk": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_code": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "engine",\n "package_name",\n "version_name",\n "version_code",\n "min_sdk",\n "target_sdk",\n "permissions",\n "manifest",\n "class_count",\n "resource_count",\n "signature_verification"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Android Package"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Search complete decompiler class names in an explicit local APK by case-sensitive substring. Empty query inventories all classes. Retains artifact and engine identity; no APK execution.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "path": "/tmp/Example.apk",\n "query": "Activity"\n }\n ],\n "properties": {\n "path": {\n "description": "Local APK path; the target is never executed",\n "minLength": 1,\n "type": "string"\n },\n "query": {\n "description": "Case-sensitive class-name substring; empty lists all classes",\n "type": "string"\n }\n },\n "required": [\n "path",\n "query"\n ],\n "type": "object"\n },\n "kind": "android-provider",\n "name": "search_android_classes",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "const": "complete",\n "type": "string"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "heap_limit_mib": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "source_revision": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "worker_count": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "version",\n "artifact_sha256",\n "source_revision",\n "worker_count",\n "heap_limit_mib"\n ],\n "type": "object"\n },\n "matches": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "query": {\n "type": "string"\n },\n "total_classes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "engine",\n "query",\n "total_classes",\n "matches",\n "coverage"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Search Android Classes"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect one exact Android class\'s parsed methods, fields and inner classes, including synthetic members. Returns display signatures and explicit unknown DEX descriptors without generating source. Overload indices remain consistent within the artifact and engine session.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "class_name": "example.MainActivity",\n "path": "/tmp/Example.apk"\n }\n ],\n "properties": {\n "class_name": {\n "description": "Exact fully qualified class name",\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "description": "Local APK path; the target is never executed",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "class_name"\n ],\n "type": "object"\n },\n "kind": "android-provider",\n "name": "inspect_android_class",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "class_name": {\n "type": "string"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "heap_limit_mib": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "source_revision": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "worker_count": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "version",\n "artifact_sha256",\n "source_revision",\n "worker_count",\n "heap_limit_mib"\n ],\n "type": "object"\n },\n "fields": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "reported_type": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "reported_type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "inner_classes": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "methods": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "dex_descriptor": {\n "type": "null"\n },\n "is_constructor": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "overload_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reported_signature": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "reported_signature",\n "overload_index",\n "is_constructor",\n "dex_descriptor"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "engine",\n "class_name",\n "methods",\n "fields",\n "inner_classes"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Android Class"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Decompile one Android method with smali fallback. Requires explicit overload_index when multiple methods share a name. Reports representation, fallback markers and source truncation; never treats display signatures as exact DEX identities.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "class_name": "example.MainActivity",\n "method_name": "onCreate",\n "path": "/tmp/Example.apk"\n }\n ],\n "properties": {\n "class_name": {\n "description": "Exact fully qualified class name",\n "minLength": 1,\n "type": "string"\n },\n "method_name": {\n "description": "Value for method name.",\n "minLength": 1,\n "type": "string"\n },\n "overload_index": {\n "description": "Explicit index among same-name methods; required when ambiguous",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "description": "Local APK path; the target is never executed",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "class_name",\n "method_name"\n ],\n "type": "object"\n },\n "kind": "android-provider",\n "name": "inspect_android_method",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "body_status": {\n "enum": [\n "available",\n "not_available"\n ],\n "type": "string"\n },\n "class_name": {\n "type": "string"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "heap_limit_mib": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "source_revision": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "worker_count": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "version",\n "artifact_sha256",\n "source_revision",\n "worker_count",\n "heap_limit_mib"\n ],\n "type": "object"\n },\n "fell_back": {\n "type": "boolean"\n },\n "markers": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "method": {\n "additionalProperties": false,\n "properties": {\n "dex_descriptor": {\n "type": "null"\n },\n "is_constructor": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "overload_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reported_signature": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "reported_signature",\n "overload_index",\n "is_constructor",\n "dex_descriptor"\n ],\n "type": "object"\n },\n "overload_count": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "representation": {\n "enum": [\n "java",\n "smali"\n ],\n "type": "string"\n },\n "source": {\n "additionalProperties": false,\n "properties": {\n "reported_total_bytes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "status",\n "text",\n "reported_total_bytes"\n ],\n "type": "object"\n }\n },\n "required": [\n "engine",\n "class_name",\n "method",\n "overload_count",\n "representation",\n "body_status",\n "fell_back",\n "markers",\n "source"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Android Method"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Trace incoming static references to an exact Android class or unique method. Overloaded method reference queries are explicitly unsupported by the current engine. Source lines and DEX offsets remain unknown; no runtime calls are inferred.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "class_name": "example.MainActivity",\n "path": "/tmp/Example.apk"\n }\n ],\n "properties": {\n "class_name": {\n "description": "Exact fully qualified class name",\n "minLength": 1,\n "type": "string"\n },\n "method_name": {\n "description": "Omit for class references; ambiguous overloaded methods are unsupported",\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "description": "Local APK path; the target is never executed",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "class_name"\n ],\n "type": "object"\n },\n "kind": "android-provider",\n "name": "trace_android_references",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "class_name": {\n "type": "string"\n },\n "coverage": {\n "const": "complete",\n "type": "string"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "heap_limit_mib": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "source_revision": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "worker_count": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "version",\n "artifact_sha256",\n "source_revision",\n "worker_count",\n "heap_limit_mib"\n ],\n "type": "object"\n },\n "method_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "containing_class": {\n "type": "string"\n },\n "dex_offset": {\n "type": "null"\n },\n "kind": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "reported_full_name": {\n "type": "string"\n },\n "source_line": {\n "type": "null"\n },\n "top_class": {\n "type": "string"\n }\n },\n "required": [\n "kind",\n "name",\n "reported_full_name",\n "containing_class",\n "top_class",\n "source_line",\n "dex_offset"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "engine",\n "class_name",\n "method_name",\n "total",\n "references",\n "coverage"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Android References"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect signature candidates in a local firmware file with Binwalk 3.1.x (verified with 3.1.0). Returns offsets, reported lengths, descriptions and raw confidence values. Length validation and runtime addresses remain unknown. No extraction or target execution. Linux, caller-supplied tool and util-linux prlimit required; 128 MiB input limit, one worker and 120-second deadline.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "path": "/tmp/firmware.bin"\n }\n ],\n "properties": {\n "path": {\n "description": "Local firmware file; never executed",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path"\n ],\n "type": "object"\n },\n "kind": "firmware-provider",\n "name": "inspect_firmware_regions",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "const": "signature_scan",\n "type": "string"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "executable_path": {\n "type": "string"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "source_revision": {\n "type": "null"\n },\n "version": {\n "type": "string"\n },\n "worker_count": {\n "const": 1,\n "type": "number"\n }\n },\n "required": [\n "name",\n "version",\n "executable_path",\n "executable_sha256",\n "source_revision",\n "worker_count"\n ],\n "type": "object"\n },\n "input_size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "regions": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "description": {\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "provider_id": {\n "type": "string"\n },\n "reported_confidence": {\n "maximum": 255,\n "minimum": 0,\n "type": "integer"\n },\n "reported_size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "signature": {\n "type": "string"\n },\n "size_basis": {\n "const": "provider_reported_validation_unknown",\n "type": "string"\n }\n },\n "required": [\n "provider_id",\n "offset",\n "reported_size",\n "signature",\n "description",\n "reported_confidence",\n "size_basis"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "engine",\n "input_size",\n "regions",\n "coverage"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Firmware Regions"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Extract caller-selected firmware bytes with Unblob 26.6.x (verified with 26.6.4) into an absent output directory. Returns verified child paths/digests, task derivations, unknown chunks and extraction diagnostics inline. Preserves the default sandbox; publishes regular files only. Linux, caller-supplied tool, format-specific extractors and util-linux prlimit required. No mounts or extracted-code execution. Select a returned native file with open_binary for Ghidra analysis; decompressed offsets are not original-file addresses.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "output_directory": "/tmp/firmware-output",\n "path": "/tmp/firmware.bin",\n "range": {\n "length": 4096,\n "offset": 64\n }\n }\n ],\n "properties": {\n "max_depth": {\n "default": 3,\n "description": "Extraction depth; reaching it yields partial analysis",\n "maximum": 10,\n "minimum": 1,\n "type": "integer"\n },\n "max_output_bytes": {\n "default": 268435456,\n "description": "Staging file-byte budget, checked during extraction; transient overshoot is possible",\n "maximum": 536870912,\n "minimum": 1,\n "type": "integer"\n },\n "max_output_files": {\n "default": 10000,\n "description": "Staging entry budget, including directories and links",\n "maximum": 100000,\n "minimum": 1,\n "type": "integer"\n },\n "output_directory": {\n "description": "Absolute, absent output directory; parent must exist",\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "description": "Local firmware file; never executed",\n "minLength": 1,\n "type": "string"\n },\n "range": {\n "additionalProperties": false,\n "description": "Explicit file-byte interval; omit to extract the whole file",\n "properties": {\n "length": {\n "description": "Value for length.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "offset",\n "length"\n ],\n "type": "object"\n }\n },\n "required": [\n "path",\n "output_directory"\n ],\n "type": "object"\n },\n "kind": "firmware-provider",\n "name": "extract_firmware",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "chunks": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "encrypted": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "handler": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "input_path": {\n "type": "string"\n },\n "provider_id": {\n "type": "string"\n },\n "range": {\n "additionalProperties": false,\n "properties": {\n "length": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "offset",\n "length"\n ],\n "type": "object"\n },\n "root_file_range": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "length": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "offset",\n "length"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "input_path",\n "provider_id",\n "range",\n "handler",\n "encrypted",\n "root_file_range"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "coverage": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n },\n "depth_limited_paths": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "derivations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "child_path": {\n "type": "string"\n },\n "handler": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "parent_file_range": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "length": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "offset",\n "length"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "parent_path": {\n "type": "string"\n },\n "provider_blob_id": {\n "type": "string"\n }\n },\n "required": [\n "parent_path",\n "child_path",\n "provider_blob_id",\n "handler",\n "parent_file_range"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "diagnostics": {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "executable_path": {\n "type": "string"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "source_revision": {\n "type": "null"\n },\n "version": {\n "type": "string"\n },\n "worker_count": {\n "const": 1,\n "type": "number"\n }\n },\n "required": [\n "name",\n "version",\n "executable_path",\n "executable_sha256",\n "source_revision",\n "worker_count"\n ],\n "type": "object"\n },\n "files": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_depth": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "original_file_range": {\n "type": "null"\n },\n "path": {\n "type": "string"\n },\n "relative_path": {\n "type": "string"\n },\n "reported_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "runtime_address": {\n "type": "null"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "relative_path",\n "path",\n "size",\n "sha256",\n "reported_sha256",\n "mime_type",\n "analysis_depth",\n "runtime_address",\n "original_file_range"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "output_directory": {\n "type": "string"\n },\n "published_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "selection": {\n "additionalProperties": false,\n "properties": {\n "length": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "offset",\n "length",\n "sha256"\n ],\n "type": "object"\n },\n "unpublished_entries": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "symlink",\n "special"\n ],\n "type": "string"\n },\n "link_target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "relative_path": {\n "type": "string"\n }\n },\n "required": [\n "relative_path",\n "kind",\n "link_target"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "engine",\n "selection",\n "output_directory",\n "coverage",\n "files",\n "unpublished_entries",\n "chunks",\n "derivations",\n "depth_limited_paths",\n "diagnostics",\n "published_bytes"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Extract Firmware"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact CIL/signature, exact declared type, name, and raw signature (decoded or not), and decoded structural method shape. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart\'s was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_b3525a33f9aecdcfeac8372301c13cb9153b189d93ef337eee0fef9425171ce3",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "artifact": {\n "byte_length": 4096,\n "format": "pe",\n "path": "/examples/00000000.dll",\n "sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "call_edges": [],\n "coverage": {\n "issues": [],\n "state": "complete"\n },\n "field_accesses": [],\n "fields": [],\n "identity_scope": {\n "requires_artifact_sha256": "0000000000000000000000000000000000000000000000000000000000000000",\n "requires_mvid": "00112233-4455-6677-8899-aabbccddeeff",\n "token_identity": "build-local"\n },\n "limitations": [],\n "member_refs": [],\n "metadata": {\n "status": "complete",\n "table_row_counts": {},\n "version": "v4.0.30319"\n },\n "methods": [],\n "module": {\n "enc_base_id": null,\n "enc_id": null,\n "generation": 0,\n "mvid": "00112233-4455-6677-8899-aabbccddeeff",\n "name": "Example.dll",\n "row_offset": 0,\n "token": "0x00000001"\n },\n "types": []\n },\n "operation": "inspect_managed_members",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n },\n "right": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_8f79f99bceb5bf3d5e5af7fd678ac07b67427756eca0784cd56ffa6b9c4dbda4",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "artifact": {\n "byte_length": 4096,\n "format": "pe",\n "path": "/examples/11111111.dll",\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "call_edges": [],\n "coverage": {\n "issues": [],\n "state": "complete"\n },\n "field_accesses": [],\n "fields": [],\n "identity_scope": {\n "requires_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "requires_mvid": "ffeeddcc-bbaa-4988-9766-554433221100",\n "token_identity": "build-local"\n },\n "limitations": [],\n "member_refs": [],\n "metadata": {\n "status": "complete",\n "table_row_counts": {},\n "version": "v4.0.30319"\n },\n "methods": [],\n "module": {\n "enc_base_id": null,\n "enc_id": null,\n "generation": 0,\n "mvid": "ffeeddcc-bbaa-4988-9766-554433221100",\n "name": "Example.dll",\n "row_offset": 0,\n "token": "0x00000001"\n },\n "types": []\n },\n "operation": "inspect_managed_members",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n }\n ],\n "properties": {\n "left": {\n "additionalProperties": false,\n "description": "Left-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "description": "Right-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "left",\n "right"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "compare_managed_members",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "const": "rea-managed-member-comparison",\n "type": "string"\n },\n "name_matching": {\n "const": "exact-signature-fallback",\n "type": "string"\n },\n "token_identity": {\n "const": "build-local",\n "type": "string"\n }\n },\n "required": [\n "name",\n "token_identity",\n "name_matching"\n ],\n "type": "object"\n },\n "comparison_id": {\n "pattern": "^mmc_[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "left_status": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n },\n "right_status": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "left_status",\n "right_status"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "fields": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_field_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "exact-il-signature",\n "exact-signature",\n "structural-method-shape",\n "field-signature"\n ],\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "enum": [\n "exact",\n "high"\n ],\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256"\n ],\n "type": "object"\n },\n "status": {\n "enum": [\n "unchanged",\n "changed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_field_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "type": "null"\n },\n "status": {\n "enum": [\n "removed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_field_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256"\n ],\n "type": "object"\n },\n "status": {\n "enum": [\n "added",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_field_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "exact-il-signature",\n "exact-signature",\n "structural-method-shape",\n "field-signature"\n ],\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "ambiguous",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "type": "null"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "left": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "fields_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "metadata_status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "methods_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "artifact_sha256",\n "mvid",\n "module_name",\n "metadata_status",\n "methods_total",\n "fields_total"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "matching": {\n "additionalProperties": false,\n "properties": {\n "ambiguous": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exact_il_signature": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exact_signature": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "field_signature": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "structural_method_shape": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unmatched": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "exact_il_signature",\n "exact_signature",\n "structural_method_shape",\n "field_signature",\n "ambiguous",\n "unmatched"\n ],\n "type": "object"\n },\n "methods": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "metadata",\n "signature",\n "cil",\n "opcode-shape",\n "call-shape",\n "field-shape",\n "exception-shape",\n "availability",\n "body-coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_method_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256",\n "normalized_il_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "exact-il-signature",\n "exact-signature",\n "structural-method-shape",\n "field-signature"\n ],\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "enum": [\n "exact",\n "high"\n ],\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256",\n "normalized_il_sha256"\n ],\n "type": "object"\n },\n "status": {\n "enum": [\n "unchanged",\n "changed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "dimensions",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "metadata",\n "signature",\n "cil",\n "opcode-shape",\n "call-shape",\n "field-shape",\n "exception-shape",\n "availability",\n "body-coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_method_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256",\n "normalized_il_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "type": "null"\n },\n "status": {\n "enum": [\n "removed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "dimensions",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "metadata",\n "signature",\n "cil",\n "opcode-shape",\n "call-shape",\n "field-shape",\n "exception-shape",\n "availability",\n "body-coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_method_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "properties": {\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256",\n "normalized_il_sha256"\n ],\n "type": "object"\n },\n "status": {\n "enum": [\n "added",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "dimensions",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "metadata",\n "signature",\n "cil",\n "opcode-shape",\n "call-shape",\n "field-shape",\n "exception-shape",\n "availability",\n "body-coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mmc_method_[a-f0-9]{64}$",\n "type": "string"\n },\n "left": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "exact-il-signature",\n "exact-signature",\n "structural-method-shape",\n "field-signature"\n ],\n "type": "string"\n },\n "candidate_left_tokens": {\n "items": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "candidate_right_tokens": {\n "items": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "ambiguous",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_tokens",\n "candidate_right_tokens"\n ],\n "type": "object"\n },\n "right": {\n "type": "null"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "evidence_links",\n "limitations",\n "item_id",\n "dimensions",\n "status",\n "left",\n "right",\n "match"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "right": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "fields_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "metadata_status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "methods_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "artifact_sha256",\n "mvid",\n "module_name",\n "metadata_status",\n "methods_total",\n "fields_total"\n ],\n "type": "object"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "changed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unchanged",\n "changed",\n "added",\n "removed",\n "unknown"\n ],\n "type": "object"\n }\n },\n "required": [\n "comparison_id",\n "algorithm",\n "left",\n "right",\n "summary",\n "matching",\n "methods",\n "fields",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Compare Managed Members"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "managed_boundaries": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_0b4af8f787323ba7fb52a35b1c21115b2c53d55d61ff9de4a16b006b818a2ba2",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "artifact": {\n "byte_length": 8192,\n "format": "pe",\n "path": "/examples/ManagedInterop.exe",\n "sha256": "6666666666666666666666666666666666666666666666666666666666666666"\n },\n "cli_native": {\n "il_only": true,\n "managed_native_header_rva": 0,\n "managed_native_header_size": 0,\n "native_entry_point": false,\n "ready_to_run_signature": false,\n "requires_32bit": false,\n "strong_name_signed": false\n },\n "coverage": {\n "issues": [],\n "state": "complete"\n },\n "identity_scope": {\n "requires_artifact_sha256": "6666666666666666666666666666666666666666666666666666666666666666",\n "requires_mvid": "00112233-4455-4677-8899-aabbccddeeff",\n "token_identity": "build-local"\n },\n "limitations": [],\n "metadata": {\n "status": "complete",\n "table_row_counts": {},\n "version": "v4.0.30319"\n },\n "module": {\n "enc_base_id": null,\n "enc_id": null,\n "generation": 0,\n "mvid": "00112233-4455-4677-8899-aabbccddeeff",\n "name": "ManagedInterop.exe",\n "row_offset": 0,\n "token": "0x00000001"\n },\n "module_refs": [\n {\n "name": "nativehelper.dll",\n "row_offset": 256,\n "token": "0x1a000001"\n }\n ],\n "native_implementations": [],\n "pinvoke_imports": [\n {\n "best_fit": "assembly-default",\n "call_convention": "cdecl",\n "char_set": "not-specified",\n "import_name": "open_native",\n "import_scope_name": "nativehelper.dll",\n "import_scope_token": "0x1a000001",\n "mapping_flags": 0,\n "mapping_flags_hex": "0x0000",\n "member_kind": "method",\n "member_name": "OpenNative",\n "member_token": "0x06000001",\n "no_mangle": true,\n "row_offset": 288,\n "supports_last_error": false,\n "throw_on_unmappable_char": "assembly-default",\n "token": "0x1c000001",\n "verification": "managed-declaration-only"\n }\n ],\n "summary": {\n "mixed_mode_or_native_header": false,\n "module_ref_count": 1,\n "native_implementation_count": 0,\n "pinvoke_import_count": 1,\n "ready_to_run": false\n }\n },\n "operation": "inspect_managed_native_boundaries",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n },\n "native_observations": [\n {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_9ec8ff855a81995540b10a08acf864e2003b588f92dd88cd2b796e3ad288fdc9",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "architectures": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "build_metadata": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "dependencies": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "endian": "little",\n "entrypoints": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "exports": {\n "exhaustive": true,\n "items": [\n {\n "address": "0x1000",\n "name": "open_native",\n "reexport": false,\n "source": "nm",\n "weak": false\n }\n ],\n "limitations": [],\n "total": 1\n },\n "file_type": "dylib",\n "flags": [],\n "format": "mach-o",\n "imports": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "limitations": [],\n "load_commands": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "provenance": [],\n "segments": {\n "exhaustive": true,\n "items": [],\n "limitations": [],\n "total": 0\n },\n "uuid": null,\n "word_size": 64\n },\n "operation": "inspect_macho",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "native-macos",\n "name": "REA native macOS provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": "x86_64",\n "digest": {\n "sha256": "7777777777777777777777777777777777777777777777777777777777777777"\n },\n "format": "mach-o",\n "local_path": "/examples/nativehelper.dll",\n "name": "nativehelper.dll"\n }\n }\n ]\n }\n ],\n "properties": {\n "managed_boundaries": {\n "additionalProperties": false,\n "description": "Value for managed boundaries.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "native_observations": {\n "description": "Native function or export Evidence records",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "managed_boundaries",\n "native_observations"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "verify_managed_native_boundaries",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "const": "rea-managed-native-verification",\n "type": "string"\n },\n "token_identity": {\n "const": "build-local",\n "type": "string"\n },\n "token_to_address_mapping": {\n "const": "not-inferred",\n "type": "string"\n }\n },\n "required": [\n "name",\n "token_identity",\n "token_to_address_mapping"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "managed_boundary": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "minLength": 1,\n "type": "string"\n },\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage_state": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "metadata_status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "native_implementations_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "pinvoke_imports_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "evidence_id",\n "artifact_sha256",\n "artifact_path",\n "mvid",\n "metadata_status",\n "pinvoke_imports_total",\n "native_implementations_total",\n "coverage_state"\n ],\n "type": "object"\n },\n "native_implementations": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "unresolved": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unresolved",\n "reason"\n ],\n "type": "object"\n },\n "native_observations": {\n "additionalProperties": false,\n "properties": {\n "accepted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symbols": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "unsupported": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "accepted",\n "unsupported",\n "symbols"\n ],\n "type": "object"\n },\n "pinvoke_imports": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "exact-export-name",\n "exact-function-name"\n ],\n "type": "string"\n },\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n },\n "prefixItems": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n }\n ],\n "type": "array"\n },\n "confidence": {\n "const": "observed",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mnv_pinvoke_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "managed": {\n "additionalProperties": false,\n "properties": {\n "call_convention": {\n "enum": [\n "not-specified",\n "winapi",\n "cdecl",\n "stdcall",\n "thiscall",\n "fastcall",\n "unknown"\n ],\n "type": "string"\n },\n "char_set": {\n "enum": [\n "not-specified",\n "ansi",\n "unicode",\n "auto",\n "unknown"\n ],\n "type": "string"\n },\n "declaration_verification": {\n "const": "managed-declaration-only",\n "type": "string"\n },\n "import_name": {\n "minLength": 1,\n "type": "string"\n },\n "import_scope_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "no_mangle": {\n "type": "boolean"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "member_token",\n "member_name",\n "import_name",\n "import_scope_name",\n "no_mangle",\n "char_set",\n "call_convention",\n "declaration_verification"\n ],\n "type": "object"\n },\n "matched_native": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n },\n "status": {\n "const": "verified",\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "managed",\n "evidence_links",\n "limitations",\n "status",\n "basis",\n "confidence",\n "matched_native",\n "candidates"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "decorated-name-candidate",\n "type": "string"\n },\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n },\n "prefixItems": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n }\n ],\n "type": "array"\n },\n "confidence": {\n "const": "inferred",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mnv_pinvoke_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "managed": {\n "additionalProperties": false,\n "properties": {\n "call_convention": {\n "enum": [\n "not-specified",\n "winapi",\n "cdecl",\n "stdcall",\n "thiscall",\n "fastcall",\n "unknown"\n ],\n "type": "string"\n },\n "char_set": {\n "enum": [\n "not-specified",\n "ansi",\n "unicode",\n "auto",\n "unknown"\n ],\n "type": "string"\n },\n "declaration_verification": {\n "const": "managed-declaration-only",\n "type": "string"\n },\n "import_name": {\n "minLength": 1,\n "type": "string"\n },\n "import_scope_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "no_mangle": {\n "type": "boolean"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "member_token",\n "member_name",\n "import_name",\n "import_scope_name",\n "no_mangle",\n "char_set",\n "call_convention",\n "declaration_verification"\n ],\n "type": "object"\n },\n "matched_native": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n },\n "status": {\n "const": "inferred",\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "managed",\n "evidence_links",\n "limitations",\n "status",\n "basis",\n "confidence",\n "matched_native",\n "candidates"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "no-native-candidate",\n "unsupported-native-evidence"\n ],\n "type": "string"\n },\n "candidates": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mnv_pinvoke_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "managed": {\n "additionalProperties": false,\n "properties": {\n "call_convention": {\n "enum": [\n "not-specified",\n "winapi",\n "cdecl",\n "stdcall",\n "thiscall",\n "fastcall",\n "unknown"\n ],\n "type": "string"\n },\n "char_set": {\n "enum": [\n "not-specified",\n "ansi",\n "unicode",\n "auto",\n "unknown"\n ],\n "type": "string"\n },\n "declaration_verification": {\n "const": "managed-declaration-only",\n "type": "string"\n },\n "import_name": {\n "minLength": 1,\n "type": "string"\n },\n "import_scope_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "no_mangle": {\n "type": "boolean"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "member_token",\n "member_name",\n "import_name",\n "import_scope_name",\n "no_mangle",\n "char_set",\n "call_convention",\n "declaration_verification"\n ],\n "type": "object"\n },\n "matched_native": {\n "type": "null"\n },\n "status": {\n "const": "unresolved",\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "managed",\n "evidence_links",\n "limitations",\n "status",\n "basis",\n "confidence",\n "matched_native",\n "candidates"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "module-mismatch",\n "type": "string"\n },\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n },\n "prefixItems": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n }\n ],\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "item_id": {\n "pattern": "^mnv_pinvoke_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "managed": {\n "additionalProperties": false,\n "properties": {\n "call_convention": {\n "enum": [\n "not-specified",\n "winapi",\n "cdecl",\n "stdcall",\n "thiscall",\n "fastcall",\n "unknown"\n ],\n "type": "string"\n },\n "char_set": {\n "enum": [\n "not-specified",\n "ansi",\n "unicode",\n "auto",\n "unknown"\n ],\n "type": "string"\n },\n "declaration_verification": {\n "const": "managed-declaration-only",\n "type": "string"\n },\n "import_name": {\n "minLength": 1,\n "type": "string"\n },\n "import_scope_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "member_token": {\n "anyOf": [\n {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "no_mangle": {\n "type": "boolean"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "member_token",\n "member_name",\n "import_name",\n "import_scope_name",\n "no_mangle",\n "char_set",\n "call_convention",\n "declaration_verification"\n ],\n "type": "object"\n },\n "matched_native": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "module_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "enum": [\n "macho-export",\n "function-dossier"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "name",\n "address",\n "module_name",\n "module_path",\n "source"\n ],\n "type": "object"\n },\n "status": {\n "const": "contradicted",\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "managed",\n "evidence_links",\n "limitations",\n "status",\n "basis",\n "confidence",\n "matched_native",\n "candidates"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "contradicted": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "inferred": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "native_body_unresolved": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unresolved": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "verified": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "verified",\n "inferred",\n "unresolved",\n "contradicted",\n "native_body_unresolved"\n ],\n "type": "object"\n },\n "verification_id": {\n "pattern": "^mnv_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "verification_id",\n "algorithm",\n "managed_boundary",\n "native_observations",\n "summary",\n "pinvoke_imports",\n "native_implementations",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Verify Managed Native Boundaries"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Import decompiler-produced managed reconstruction against authenticated inspect_managed_members Evidence. The workflow locks each method to artifact SHA-256, MVID, metadata token, signature hash, and the limited decoded-IL tuple hash, records the decompiler identity and options, and marks C# or pseudocode as analyst inference rather than canonical byte observation.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "decompiler": {\n "executable_sha256": null,\n "family": "ilspy",\n "name": "ilspycmd",\n "options": [\n "--disable-updatecheck",\n "--type",\n "Example.Program"\n ],\n "version": "9.1.0.7988"\n },\n "methods": [\n {\n "normalized_il_sha256": "5555555555555555555555555555555555555555555555555555555555555555",\n "reconstruction": {\n "end_line": 1,\n "kind": "decompiled-csharp",\n "language": "csharp",\n "source_path": "/examples/Example.Program.cs",\n "start_line": 1,\n "text": "internal static void Main() { }"\n },\n "signature_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "token": "0x06000001"\n }\n ],\n "notes": [\n "Synthetic example only; decompiler output is imported as inference."\n ],\n "static_members": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_cf07661a99083e446ac5f18af3ca331cb1af7c3cd6cb7c9dc8bff880a1ed8bb9",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "artifact": {\n "byte_length": 4096,\n "format": "pe",\n "path": "/examples/22222222.dll",\n "sha256": "2222222222222222222222222222222222222222222222222222222222222222"\n },\n "call_edges": [],\n "coverage": {\n "issues": [],\n "state": "complete"\n },\n "field_accesses": [],\n "fields": [],\n "identity_scope": {\n "requires_artifact_sha256": "2222222222222222222222222222222222222222222222222222222222222222",\n "requires_mvid": "11112222-3333-4444-9555-666677778888",\n "token_identity": "build-local"\n },\n "limitations": [],\n "member_refs": [],\n "metadata": {\n "status": "complete",\n "table_row_counts": {},\n "version": "v4.0.30319"\n },\n "methods": [\n {\n "body": {\n "anchors": [],\n "decoded_instruction_count": 1,\n "exception_regions": [],\n "file_offset": 512,\n "header_format": "tiny",\n "il_sha256": "4444444444444444444444444444444444444444444444444444444444444444",\n "il_size": 1,\n "init_locals": false,\n "instruction_count": 1,\n "issue": null,\n "local_var_sig_token": null,\n "max_stack": 8,\n "normalized_il_sha256": "5555555555555555555555555555555555555555555555555555555555555555",\n "opcode_counts": {\n "ret": 1\n },\n "rva": 8192,\n "status": "present",\n "truncated_instructions": 0\n },\n "declaring_type": "Example.Program",\n "declaring_type_token": "0x02000001",\n "flags": 22,\n "impl_flags": 0,\n "name": "Main",\n "row_offset": 128,\n "rva": 8192,\n "signature": {\n "calling_convention": "default",\n "field_type": null,\n "generic_parameter_count": 0,\n "issue": null,\n "kind": "method",\n "parameter_count": 0,\n "parameter_types": [],\n "parse_status": "decoded",\n "raw_length": 3,\n "raw_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "return_type": "void"\n },\n "token": "0x06000001"\n }\n ],\n "module": {\n "enc_base_id": null,\n "enc_id": null,\n "generation": 0,\n "mvid": "11112222-3333-4444-9555-666677778888",\n "name": "Example.dll",\n "row_offset": 0,\n "token": "0x00000001"\n },\n "types": []\n },\n "operation": "inspect_managed_members",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n }\n ],\n "properties": {\n "decompiler": {\n "additionalProperties": false,\n "description": "Value for decompiler.",\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Exact SHA-256 digest of executable."\n },\n "family": {\n "description": "Value for family.",\n "enum": [\n "ilspy",\n "dnspy",\n "dnlib",\n "mono-cecil",\n "other"\n ],\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "options": {\n "default": [],\n "description": "Value for options.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "version": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for version."\n }\n },\n "required": [\n "name",\n "family"\n ],\n "type": "object"\n },\n "methods": {\n "description": "Value for methods.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact SHA-256 digest of normalized il."\n },\n "reconstruction": {\n "additionalProperties": false,\n "description": "Value for reconstruction.",\n "properties": {\n "end_line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for end line."\n },\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "decompiled-csharp",\n "decompiled-il",\n "semantic-pseudocode"\n ],\n "type": "string"\n },\n "language": {\n "description": "Value for language.",\n "enum": [\n "csharp",\n "il",\n "pseudocode"\n ],\n "type": "string"\n },\n "source_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Local filesystem path for source."\n },\n "start_line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for start line."\n },\n "text": {\n "description": "Value for text.",\n "minLength": 1,\n "type": "string"\n },\n "text_sha256": {\n "description": "Exact SHA-256 digest of text.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "language",\n "text"\n ],\n "type": "object"\n },\n "signature_sha256": {\n "description": "Exact SHA-256 digest of signature.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "description": "Value for token.",\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n }\n },\n "required": [\n "token",\n "signature_sha256",\n "normalized_il_sha256",\n "reconstruction"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "notes": {\n "default": [],\n "description": "Value for notes.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "static_members": {\n "additionalProperties": false,\n "description": "Value for static members.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "static_members",\n "decompiler",\n "methods"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "import_managed_reconstruction",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "decompiler": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "family": {\n "enum": [\n "ilspy",\n "dnspy",\n "dnlib",\n "mono-cecil",\n "other"\n ],\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "options": {\n "default": [],\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "options_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n }\n },\n "required": [\n "name",\n "version",\n "family",\n "executable_sha256",\n "options",\n "options_sha256"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 1,\n "minItems": 1,\n "type": "array"\n },\n "executed": {\n "const": false,\n "type": "boolean"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "methods": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "body_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "declaring_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "il_size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "name": {\n "type": "string"\n },\n "normalized_il_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "reconstruction": {\n "additionalProperties": false,\n "properties": {\n "end_line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "decompiled-csharp",\n "decompiled-il",\n "semantic-pseudocode"\n ],\n "type": "string"\n },\n "language": {\n "enum": [\n "csharp",\n "il",\n "pseudocode"\n ],\n "type": "string"\n },\n "line_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "start_line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "text": {\n "minLength": 1,\n "type": "string"\n },\n "text_length": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "text_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "language",\n "text",\n "text_sha256",\n "text_length",\n "line_count",\n "source_path",\n "start_line",\n "end_line"\n ],\n "type": "object"\n },\n "signature_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "token": {\n "pattern": "^0x[0-9a-f]{8}$",\n "type": "string"\n },\n "validation": {\n "additionalProperties": false,\n "properties": {\n "canonical_observation": {\n "const": false,\n "type": "boolean"\n },\n "confidence_floor": {\n "const": "inference",\n "type": "string"\n },\n "exact_build_required": {\n "const": true,\n "type": "boolean"\n },\n "matched_static_member": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "matched_static_member",\n "exact_build_required",\n "canonical_observation",\n "confidence_floor"\n ],\n "type": "object"\n }\n },\n "required": [\n "token",\n "declaring_type",\n "name",\n "signature_sha256",\n "normalized_il_sha256",\n "body_sha256",\n "il_size",\n "reconstruction",\n "validation"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "notes": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "phase": {\n "const": "reconstruction-import",\n "type": "string"\n },\n "reconstruction_id": {\n "pattern": "^mre_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_observation": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "minLength": 1,\n "type": "string"\n },\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "byte_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "metadata_status": {\n "enum": [\n "absent",\n "complete",\n "partial",\n "malformed"\n ],\n "type": "string"\n },\n "module_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "mvid": {\n "anyOf": [\n {\n "pattern": "^[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "artifact_sha256",\n "artifact_path",\n "byte_length",\n "module_name",\n "mvid",\n "metadata_status"\n ],\n "type": "object"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "decompiled_csharp_methods": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "decompiled_il_methods": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "imported_methods": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "pseudocode_methods": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total_text_bytes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "imported_methods",\n "decompiled_csharp_methods",\n "decompiled_il_methods",\n "pseudocode_methods",\n "total_text_bytes"\n ],\n "type": "object"\n }\n },\n "required": [\n "reconstruction_id",\n "phase",\n "executed",\n "static_observation",\n "decompiler",\n "summary",\n "methods",\n "notes",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Import Managed Reconstruction"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Project authenticated managed artifact, member, and native-boundary Evidence into the provider-neutral application graph without executing managed code, loading assemblies, or translating managed metadata tokens to native addresses. The graph preserves managed static observations as managed-specific nodes linked to source Evidence.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "managed_artifact": {\n "additionalProperties": false,\n "description": "Value for managed artifact.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "managed_members": {\n "additionalProperties": false,\n "description": "Value for managed members.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "managed_native_boundaries": {\n "additionalProperties": false,\n "description": "Value for managed native boundaries.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "managed_artifact"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "managed_artifact": {\n "additionalProperties": false,\n "description": "Value for managed artifact.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "managed_members": {\n "additionalProperties": false,\n "description": "Value for managed members.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "managed_native_boundaries": {\n "additionalProperties": false,\n "description": "Value for managed native boundaries.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "managed_members"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "managed_artifact": {\n "additionalProperties": false,\n "description": "Value for managed artifact.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "managed_members": {\n "additionalProperties": false,\n "description": "Value for managed members.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "managed_native_boundaries": {\n "additionalProperties": false,\n "description": "Value for managed native boundaries.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "managed_native_boundaries"\n ],\n "type": "object"\n }\n ],\n "examples": [\n {\n "managed_members": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_cf07661a99083e446ac5f18af3ca331cb1af7c3cd6cb7c9dc8bff880a1ed8bb9",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "artifact": {\n "byte_length": 4096,\n "format": "pe",\n "path": "/examples/22222222.dll",\n "sha256": "2222222222222222222222222222222222222222222222222222222222222222"\n },\n "call_edges": [],\n "coverage": {\n "issues": [],\n "state": "complete"\n },\n "field_accesses": [],\n "fields": [],\n "identity_scope": {\n "requires_artifact_sha256": "2222222222222222222222222222222222222222222222222222222222222222",\n "requires_mvid": "11112222-3333-4444-9555-666677778888",\n "token_identity": "build-local"\n },\n "limitations": [],\n "member_refs": [],\n "metadata": {\n "status": "complete",\n "table_row_counts": {},\n "version": "v4.0.30319"\n },\n "methods": [\n {\n "body": {\n "anchors": [],\n "decoded_instruction_count": 1,\n "exception_regions": [],\n "file_offset": 512,\n "header_format": "tiny",\n "il_sha256": "4444444444444444444444444444444444444444444444444444444444444444",\n "il_size": 1,\n "init_locals": false,\n "instruction_count": 1,\n "issue": null,\n "local_var_sig_token": null,\n "max_stack": 8,\n "normalized_il_sha256": "5555555555555555555555555555555555555555555555555555555555555555",\n "opcode_counts": {\n "ret": 1\n },\n "rva": 8192,\n "status": "present",\n "truncated_instructions": 0\n },\n "declaring_type": "Example.Program",\n "declaring_type_token": "0x02000001",\n "flags": 22,\n "impl_flags": 0,\n "name": "Main",\n "row_offset": 128,\n "rva": 8192,\n "signature": {\n "calling_convention": "default",\n "field_type": null,\n "generic_parameter_count": 0,\n "issue": null,\n "kind": "method",\n "parameter_count": 0,\n "parameter_types": [],\n "parse_status": "decoded",\n "raw_length": 3,\n "raw_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "return_type": "void"\n },\n "token": "0x06000001"\n }\n ],\n "module": {\n "enc_base_id": null,\n "enc_id": null,\n "generation": 0,\n "mvid": "11112222-3333-4444-9555-666677778888",\n "name": "Example.dll",\n "row_offset": 0,\n "token": "0x00000001"\n },\n "types": []\n },\n "operation": "inspect_managed_members",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-dotnet-static",\n "name": "REA managed static analysis provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n }\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "project_managed_application_graph",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "graph": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "edge_id": {\n "pattern": "^jag_edge_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "contains",\n "loads",\n "imports",\n "maps_to",\n "exposes",\n "sends",\n "invokes",\n "handles",\n "calls",\n "persists_to",\n "observed_as",\n "changed_from"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "properties",\n "evidence",\n "edge_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "identity": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "global-exact",\n "type": "string"\n },\n "strategy": {\n "const": "content-digest",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "original_source": {\n "type": "string"\n },\n "source_map_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "stability": {\n "const": "source-map-exact",\n "type": "string"\n },\n "strategy": {\n "const": "source-map-original",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "source_map_sha256",\n "original_source",\n "source_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "canonical-path",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "minLength": 1,\n "type": "string"\n },\n "basis": {\n "items": {\n "enum": [\n "syntax-tree",\n "control-flow",\n "imports",\n "exports",\n "literals",\n "api-usage"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "cross-version-inference",\n "type": "string"\n },\n "strategy": {\n "const": "structural-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "algorithm",\n "fingerprint_sha256",\n "basis"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "key": {\n "type": "string"\n },\n "namespace": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "artifact-local-key",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "namespace",\n "key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_key": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "capture-only",\n "type": "string"\n },\n "strategy": {\n "const": "runtime-instance",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "capture_sha256",\n "runtime_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observation_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "observation-only",\n "type": "string"\n },\n "strategy": {\n "const": "observation-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "observation_sha256",\n "scope"\n ],\n "type": "object"\n }\n ]\n },\n "kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "observations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "observation-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "observation_id": {\n "pattern": "^jag_observation_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "label",\n "properties",\n "evidence",\n "observation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "kind",\n "identity",\n "observations"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "root_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "schema": {\n "const": "JavaScriptApplicationGraph",\n "type": "string"\n }\n },\n "required": [\n "schema",\n "root_node_ids",\n "nodes",\n "edges",\n "coverage",\n "limitations",\n "graph_id"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "projection_id": {\n "pattern": "^magp_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_evidence": {\n "additionalProperties": false,\n "properties": {\n "managed_artifact_evidence_id": {\n "anyOf": [\n {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "managed_members_evidence_id": {\n "anyOf": [\n {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "managed_native_boundaries_evidence_id": {\n "anyOf": [\n {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "managed_artifact_evidence_id",\n "managed_members_evidence_id",\n "managed_native_boundaries_evidence_id"\n ],\n "type": "object"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "assemblies": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "fields": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "graph_edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "graph_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "methods": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "modules": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "native_implementations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "pinvoke_imports": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "types": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "graph_nodes",\n "graph_edges",\n "assemblies",\n "modules",\n "types",\n "methods",\n "fields",\n "pinvoke_imports",\n "native_implementations"\n ],\n "type": "object"\n }\n },\n "required": [\n "projection_id",\n "root_artifact_sha256",\n "source_evidence",\n "summary",\n "graph",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Project Managed Application Graph"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "List page targets exposed by the selected loopback Chrome DevTools Protocol endpoint. Optionally filter by exact origin. URLs preserve query values and fragments; only URL userinfo credentials are removed.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "list_browser_targets",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "excluded": {\n "additionalProperties": false,\n "properties": {\n "disallowed_origin": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "non_page": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unsupported_url": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "disallowed_origin",\n "unsupported_url",\n "non_page"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "targets": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "origin": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n },\n "title": {\n "type": "string"\n },\n "type": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "title",\n "url",\n "origin",\n "attached"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "browser",\n "targets",\n "excluded",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Browser Targets"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Passively inspect one selected page target through CDP without evaluating JavaScript, navigating, clicking, closing, or mutating the page. If no origin filter is supplied, capture is scoped to the target\'s current origin. Returns DOM structure, accessibility, scripts, resources, attach-window network and console metadata, workers, and redacted storage inventory as Evidence.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "include_accessibility_text": {\n "default": false,\n "description": "Whether to include accessibility text in the result.",\n "type": "boolean"\n },\n "include_console_text": {\n "default": false,\n "description": "Whether to include console text in the result.",\n "type": "boolean"\n },\n "include_json_body_shapes": {\n "default": false,\n "description": "Whether to include json body shapes in the result.",\n "type": "boolean"\n },\n "include_script_sources": {\n "const": false,\n "default": false,\n "description": "Whether to include script sources in the result.",\n "type": "boolean"\n },\n "include_storage_fingerprints": {\n "default": false,\n "description": "Whether to include storage fingerprints in the result.",\n "type": "boolean"\n },\n "include_storage_keys": {\n "default": false,\n "description": "Whether to include storage keys in the result.",\n "type": "boolean"\n },\n "include_websocket_shapes": {\n "default": false,\n "description": "Whether to include websocket shapes in the result.",\n "type": "boolean"\n },\n "observation_ms": {\n "default": 500,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "include_accessibility_text": {\n "default": false,\n "description": "Whether to include accessibility text in the result.",\n "type": "boolean"\n },\n "include_console_text": {\n "default": false,\n "description": "Whether to include console text in the result.",\n "type": "boolean"\n },\n "include_json_body_shapes": {\n "default": false,\n "description": "Whether to include json body shapes in the result.",\n "type": "boolean"\n },\n "include_script_sources": {\n "const": true,\n "default": true,\n "description": "Whether to include script sources in the result.",\n "type": "boolean"\n },\n "include_storage_fingerprints": {\n "default": false,\n "description": "Whether to include storage fingerprints in the result.",\n "type": "boolean"\n },\n "include_storage_keys": {\n "default": false,\n "description": "Whether to include storage keys in the result.",\n "type": "boolean"\n },\n "include_websocket_shapes": {\n "default": false,\n "description": "Whether to include websocket shapes in the result.",\n "type": "boolean"\n },\n "observation_ms": {\n "default": 500,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n }\n ],\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "inspect_web_page",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "accessibility": {\n "additionalProperties": false,\n "properties": {\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "description": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "ignored": {\n "type": "boolean"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "node_id": {\n "type": "string"\n },\n "parent_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "states": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "number"\n },\n {\n "type": "string"\n }\n ]\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "parent_id",\n "role",\n "name",\n "description",\n "ignored",\n "states"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "text_capture": {\n "additionalProperties": false,\n "properties": {\n "excluded_fields": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "retained_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "not_approved",\n "included",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "retained_bytes",\n "excluded_fields"\n ],\n "type": "object"\n },\n "total_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "text_capture",\n "nodes"\n ],\n "type": "object"\n },\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "capture_window": {\n "additionalProperties": false,\n "properties": {\n "ended_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "observation_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "started_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n }\n },\n "required": [\n "started_at",\n "ended_at",\n "observation_ms"\n ],\n "type": "object"\n },\n "completeness": {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "console": {\n "additionalProperties": false,\n "properties": {\n "coverage_started_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "events": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "argument_types": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "text_capture": {\n "additionalProperties": false,\n "properties": {\n "retained_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "not_approved",\n "included"\n ],\n "type": "string"\n },\n "values": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "argument_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text": {\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "argument_index",\n "type",\n "text"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "values",\n "retained_bytes"\n ],\n "type": "object"\n },\n "timestamp": {\n "type": "number"\n },\n "type": {\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "type",\n "timestamp",\n "argument_types",\n "url",\n "line",\n "column",\n "text_capture"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "prior_activity_available": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "events",\n "coverage_started_at",\n "prior_activity_available"\n ],\n "type": "object"\n },\n "dom": {\n "additionalProperties": false,\n "properties": {\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attribute_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_name": {\n "type": "string"\n },\n "node_type": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_value_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parent_index": {\n "maximum": 9007199254740991,\n "minimum": -1,\n "type": "integer"\n }\n },\n "required": [\n "index",\n "parent_index",\n "node_type",\n "node_name",\n "node_value_length",\n "attribute_names"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "nodes"\n ],\n "type": "object"\n },\n "frames": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "type": "string"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "frame_id",\n "parent_frame_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "metadata": {\n "additionalProperties": false,\n "properties": {\n "agent_hints": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "declaration": {\n "type": "string"\n },\n "mechanism": {\n "enum": [\n "link_rel",\n "dom_link_rel",\n "well_known_resource",\n "response_header"\n ],\n "type": "string"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "mechanism",\n "declaration",\n "url",\n "trust"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "dom_urls": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attribute": {\n "enum": [\n "href",\n "src",\n "action",\n "formaction",\n "poster"\n ],\n "type": "string"\n },\n "destination_scope": {\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n "node_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "node_index",\n "attribute",\n "url",\n "destination_scope"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "excluded_dom_urls": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "headers_allowlisted": {\n "const": true,\n "type": "boolean"\n },\n "responses": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "content_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "content_length": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "csp": {\n "additionalProperties": false,\n "properties": {\n "directives": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "keyword",\n "scheme",\n "approved_origin",\n "external_origin",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "sources"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "hash_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nonce_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "directives",\n "nonce_count",\n "hash_count"\n ],\n "type": "object"\n },\n "links": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "as": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "crossorigin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "destination_scope": {\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n "href": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "rel": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "href",\n "destination_scope",\n "rel",\n "as",\n "type",\n "crossorigin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "policies": {\n "additionalProperties": false,\n "properties": {\n "coep": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "coop": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "corp": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "permissions_policy_features": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "referrer_policy": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "x_content_type_options": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "coop",\n "coep",\n "corp",\n "referrer_policy",\n "x_content_type_options",\n "permissions_policy_features"\n ],\n "type": "object"\n },\n "request_id": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "mime_type",\n "content_length",\n "content_encoding",\n "csp",\n "links",\n "policies"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "responses",\n "dom_urls",\n "agent_hints",\n "excluded_dom_urls",\n "headers_allowlisted"\n ],\n "type": "object"\n },\n "network": {\n "additionalProperties": false,\n "properties": {\n "coverage_started_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "prior_activity_available": {\n "const": false,\n "type": "boolean"\n },\n "requests": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "body_shapes": {\n "additionalProperties": false,\n "properties": {\n "request": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "max_depth_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observations": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "types": {\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "response": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "max_depth_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observations": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "types": {\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "not_approved",\n "included",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "request",\n "response"\n ],\n "type": "object"\n },\n "encoded_data_length": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "initiator": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "type": {\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "type",\n "url",\n "line",\n "column"\n ],\n "type": "object"\n },\n "method": {\n "type": "string"\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "request_id": {\n "type": "string"\n },\n "resource_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "origin",\n "method",\n "resource_type",\n "status",\n "mime_type",\n "encoded_data_length",\n "initiator",\n "body_shapes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "websocket_events": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "direction": {\n "enum": [\n "sent",\n "received"\n ],\n "type": "string"\n },\n "opcode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_shape": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "format": {\n "enum": [\n "json",\n "text",\n "binary"\n ],\n "type": "string"\n },\n "json_shape": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "max_depth_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observations": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "types": {\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "format",\n "json_shape"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "request_id": {\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "direction",\n "opcode",\n "payload_bytes",\n "payload_shape"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "requests",\n "websocket_events",\n "coverage_started_at",\n "prior_activity_available"\n ],\n "type": "object"\n },\n "resources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "content_size": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "mime_type": {\n "type": "string"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_key": {\n "pattern": "^res_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "resource_key",\n "url",\n "origin",\n "type",\n "mime_type",\n "content_size"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "scripts": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "cdp_hash": {\n "type": "string"\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "is_module": {\n "type": "boolean"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_reconciliation": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "resource_key": {\n "type": "string"\n },\n "status": {\n "const": "exact",\n "type": "string"\n }\n },\n "required": [\n "status",\n "resource_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "candidate_resource_keys": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "ambiguous",\n "type": "string"\n }\n },\n "required": [\n "status",\n "candidate_resource_keys"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "const": "no_exact_sanitized_url",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "script_key": {\n "pattern": "^scr_[a-f0-9]{64}(?:_[0-9]+)?$",\n "type": "string"\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "included": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "included",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n "included": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "included",\n "artifact"\n ],\n "type": "object"\n }\n ]\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "frame_id",\n "url",\n "origin",\n "cdp_hash",\n "length",\n "is_module",\n "language",\n "source_map_url",\n "resource_reconciliation",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n },\n "storage": {\n "additionalProperties": false,\n "properties": {\n "cache_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "content_fingerprints": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "complete": {\n "type": "boolean"\n },\n "identity_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "enum": [\n "cookie",\n "local_storage",\n "session_storage",\n "indexed_db_schema",\n "indexed_db_record",\n "cache_entry"\n ],\n "type": "string"\n },\n "value_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "scope",\n "identity_sha256",\n "value_sha256",\n "complete"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "fingerprint_algorithm": {\n "const": "sha256",\n "default": "sha256",\n "type": "string"\n },\n "fingerprints_complete": {\n "default": false,\n "type": "boolean"\n },\n "indexed_db_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "local_storage_keys": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "origin": {\n "type": "string"\n },\n "quota_bytes": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "session_storage_keys": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "usage_bytes": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "values_redacted": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "origin",\n "usage_bytes",\n "quota_bytes",\n "local_storage_keys",\n "session_storage_keys",\n "indexed_db_names",\n "cache_names",\n "content_fingerprints",\n "fingerprint_algorithm",\n "fingerprints_complete",\n "values_redacted"\n ],\n "type": "object"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "origin": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n },\n "title": {\n "type": "string"\n },\n "type": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "title",\n "url",\n "origin",\n "attached"\n ],\n "type": "object"\n },\n "workers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "opener_target_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "target_id": {\n "type": "string"\n },\n "type": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "url",\n "origin",\n "attached",\n "opener_target_id",\n "parent_frame_id"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "browser",\n "target",\n "capture_window",\n "completeness",\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "resources",\n "network",\n "console",\n "workers",\n "metadata",\n "storage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Web Page"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Capture JavaScript source from one selected CDP page and statically derive a chunk graph, route and endpoint candidates, vendor fingerprints, page-declared WebMCP metadata, and optionally fetch source maps from the selected target origin or explicitly requested origins. JavaScript is parsed but never executed.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "fetch_source_maps": false,\n "include_accessibility_text": false,\n "include_script_sources": true,\n "include_storage_keys": false,\n "observation_ms": 500,\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "fetch_source_maps": {\n "default": false,\n "description": "Value for fetch source maps.",\n "type": "boolean"\n },\n "include_accessibility_text": {\n "default": false,\n "description": "Whether to include accessibility text in the result.",\n "type": "boolean"\n },\n "include_console_text": {\n "default": false,\n "description": "Whether to include console text in the result.",\n "type": "boolean"\n },\n "include_json_body_shapes": {\n "default": false,\n "description": "Whether to include json body shapes in the result.",\n "type": "boolean"\n },\n "include_script_sources": {\n "const": true,\n "default": true,\n "description": "Whether to include script sources in the result.",\n "type": "boolean"\n },\n "include_storage_fingerprints": {\n "default": false,\n "description": "Whether to include storage fingerprints in the result.",\n "type": "boolean"\n },\n "include_storage_keys": {\n "default": false,\n "description": "Whether to include storage keys in the result.",\n "type": "boolean"\n },\n "include_websocket_shapes": {\n "default": false,\n "description": "Whether to include websocket shapes in the result.",\n "type": "boolean"\n },\n "observation_ms": {\n "default": 500,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "analyze_web_bundle",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "capture": {\n "additionalProperties": false,\n "properties": {\n "scripts_analyzed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "source_artifacts": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text_available": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text_available"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target_url": {\n "type": "string"\n }\n },\n "required": [\n "target_url",\n "scripts_observed",\n "scripts_analyzed",\n "source_artifacts"\n ],\n "type": "object"\n },\n "completeness": {\n "additionalProperties": false,\n "properties": {\n "parse_failures": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parsed_scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n },\n "visited_ast_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "parsed_scripts",\n "parse_failures",\n "visited_ast_nodes"\n ],\n "type": "object"\n },\n "inferences": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "detector": {\n "type": "string"\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "artifact_sha256",\n "line",\n "column",\n "detector"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "confidence": {\n "enum": [\n "low",\n "medium",\n "high"\n ],\n "type": "string"\n },\n "kind": {\n "enum": [\n "vendor_fingerprint",\n "route_framework",\n "bundle_runtime"\n ],\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value",\n "confidence",\n "basis"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "observations": {\n "additionalProperties": false,\n "properties": {\n "chunks": {\n "additionalProperties": false,\n "properties": {\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "from_script_key": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "static_import",\n "dynamic_import",\n "require",\n "worker_import"\n ],\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "line",\n "column"\n ],\n "type": "object"\n },\n "resolved_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "specifier": {\n "type": "string"\n }\n },\n "required": [\n "from_script_key",\n "kind",\n "specifier",\n "resolved_url",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "script_key": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "url",\n "artifact_sha256",\n "bytes"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "nodes",\n "edges"\n ],\n "type": "object"\n },\n "endpoints": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "location": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "line",\n "column"\n ],\n "type": "object"\n },\n "mechanism": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "value",\n "mechanism",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "routes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "location": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "line",\n "column"\n ],\n "type": "object"\n },\n "mechanism": {\n "type": "string"\n },\n "value": {\n "type": "string"\n }\n },\n "required": [\n "value",\n "mechanism",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_maps": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n "declared_url": {\n "type": "string"\n },\n "limitation": {\n "type": "null"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "generated_column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "generated_line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "original_column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "original_line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source": {\n "type": "string"\n }\n },\n "required": [\n "generated_line",\n "generated_column",\n "source",\n "original_line",\n "original_column",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "original_module_edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "from_source": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "static_import",\n "dynamic_import",\n "require"\n ],\n "type": "string"\n },\n "resolved_source": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "specifier": {\n "type": "string"\n }\n },\n "required": [\n "from_source",\n "kind",\n "specifier",\n "resolved_source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "original_sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "source": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "artifact"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "script_key": {\n "type": "string"\n },\n "status": {\n "const": "included",\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "declared_url",\n "artifact",\n "original_sources",\n "original_module_edges",\n "mappings",\n "status",\n "limitation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n "declared_url": {\n "type": "string"\n },\n "limitation": {\n "type": "string"\n },\n "mappings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "generated_column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "generated_line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "original_column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "original_line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source": {\n "type": "string"\n }\n },\n "required": [\n "generated_line",\n "generated_column",\n "source",\n "original_line",\n "original_column",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "original_module_edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "from_source": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "static_import",\n "dynamic_import",\n "require"\n ],\n "type": "string"\n },\n "resolved_source": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "specifier": {\n "type": "string"\n }\n },\n "required": [\n "from_source",\n "kind",\n "specifier",\n "resolved_source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "original_sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "source": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "artifact"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "script_key": {\n "type": "string"\n },\n "status": {\n "const": "partial",\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "declared_url",\n "artifact",\n "original_sources",\n "original_module_edges",\n "mappings",\n "status",\n "limitation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "type": "null"\n },\n "declared_url": {\n "type": "string"\n },\n "limitation": {\n "type": "string"\n },\n "mappings": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "original_module_edges": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "original_sources": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "script_key": {\n "type": "string"\n },\n "status": {\n "enum": [\n "fetch_failed",\n "invalid",\n "policy_filtered"\n ],\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "declared_url",\n "artifact",\n "original_sources",\n "original_module_edges",\n "mappings",\n "status",\n "limitation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "processed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "not_requested",\n "included",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "requested",\n "processed",\n "items"\n ],\n "type": "object"\n },\n "webmcp_declarations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "description": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "line",\n "column"\n ],\n "type": "object"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "schema_property_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "type": "string"\n }\n },\n "required": [\n "name",\n "description",\n "schema_property_names",\n "trust",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "chunks",\n "routes",\n "endpoints",\n "webmcp_declarations",\n "source_maps"\n ],\n "type": "object"\n },\n "unknowns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "affected_script_keys": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "dimension": {\n "type": "string"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "dimension",\n "reason",\n "affected_script_keys"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "capture",\n "observations",\n "inferences",\n "unknowns",\n "completeness",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Analyze Web Bundle"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Arm a CDP observation window of the requested duration while the user operates the page. By default it follows the selected target\'s current origin; an explicit origin list controls which origins may be retained. Records navigation, redirect, lifecycle, and failure events.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "observation_ms": 10000,\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "observation_ms": {\n "default": 10000,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "observe_web_session",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "completeness": {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "final_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "initial_url": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "initial_url",\n "final_url"\n ],\n "type": "object"\n },\n "timeline": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "destination_scope": {\n "anyOf": [\n {\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "detail": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "loader_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "request_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "timestamp": {\n "minimum": 0,\n "type": "number"\n },\n "type": {\n "enum": [\n "navigation_requested",\n "navigation_committed",\n "same_origin_reload",\n "same_document_navigation",\n "redirect",\n "load_failed",\n "lifecycle",\n "target_terminated"\n ],\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "sequence",\n "type",\n "timestamp",\n "frame_id",\n "loader_id",\n "request_id",\n "url",\n "destination_scope",\n "detail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "armed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "end_reason": {\n "enum": [\n "window_elapsed",\n "target_left_scope",\n "target_terminated"\n ],\n "type": "string"\n },\n "ended_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "requested_ms": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "armed_at",\n "ended_at",\n "requested_ms",\n "end_reason"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "window",\n "timeline",\n "completeness",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Observe Web Session"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Passively inventory every page-registered WebMCP tool and its complete structural input-schema summary using the experimental CDP WebMCP domain. Metadata is page-declared-untrusted; REA never registers or invokes discovered tools.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "observation_ms": 100,\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "observation_ms": {\n "default": 100,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "discover_webmcp_tools",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "completeness": {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "available",\n "unavailable"\n ],\n "type": "string"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "tools": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "annotations": {\n "additionalProperties": false,\n "properties": {\n "autosubmit": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "read_only": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "untrusted_content": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "read_only",\n "untrusted_content",\n "autosubmit"\n ],\n "type": "object"\n },\n "declaration_kind": {\n "enum": [\n "declarative",\n "imperative"\n ],\n "type": "string"\n },\n "description": {\n "type": "string"\n },\n "frame_id": {\n "type": "string"\n },\n "frame_url": {\n "type": "string"\n },\n "input_schema_shape": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "max_depth_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observations": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "types": {\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "owner_origin": {\n "type": "string"\n },\n "registration_source": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "line",\n "column"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "tool_key": {\n "pattern": "^webmcp_[a-f0-9]{64}$",\n "type": "string"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "type": "string"\n }\n },\n "required": [\n "tool_key",\n "name",\n "description",\n "frame_id",\n "frame_url",\n "owner_origin",\n "declaration_kind",\n "input_schema_shape",\n "annotations",\n "registration_source",\n "trust"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "status",\n "tools",\n "completeness",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Discover Webmcp Tools"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare passive web captures by providing before and after, or compare recorded scenarios by providing before_scenario and after_scenario with an optional normalization policy. Use exactly one input group. Scenario comparison aligns exact step IDs, records deterministic literal normalization, and returns every artifact-level difference plus alignment failures inline. Missing or truncated evidence never proves equality.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "after_scenario": {\n "browser": {\n "cleanup": "disconnected-external",\n "mode": "connect",\n "process_ownership": "external",\n "product": "Chromium",\n "version": "149"\n },\n "completeness": {\n "equality_eligible": true,\n "missing_sections": [],\n "status": "complete",\n "truncated_sections": []\n },\n "duration_ms": 10,\n "events": {\n "dropped": 0,\n "items": [],\n "retained": 0\n },\n "limitations": [],\n "scenario": {\n "action_count": 1,\n "secret_references": [],\n "start_origin": "https://app.example.test"\n },\n "steps": [\n {\n "action": "scenario_start",\n "after_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "artifacts": {\n "accessibility": {\n "state": "not_requested"\n },\n "dom": {\n "state": "not_requested"\n },\n "history": {\n "state": "not_requested"\n },\n "screenshot": {\n "state": "not_requested"\n },\n "storage": {\n "state": "not_requested"\n },\n "url": {\n "state": "not_requested"\n }\n },\n "before_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "completeness": {\n "equality_eligible": true,\n "missing_sections": [],\n "status": "complete",\n "truncated_sections": []\n },\n "elapsed_ms": 0,\n "error": null,\n "event_sequence_end": 0,\n "event_sequence_start": 1,\n "status": "completed",\n "step_id": "scenario_start",\n "step_index": 0\n },\n {\n "action": "click",\n "after_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "artifacts": {\n "accessibility": {\n "state": "not_requested"\n },\n "dom": {\n "state": "not_requested"\n },\n "history": {\n "state": "not_requested"\n },\n "screenshot": {\n "state": "not_requested"\n },\n "storage": {\n "state": "not_requested"\n },\n "url": {\n "state": "not_requested"\n }\n },\n "before_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "completeness": {\n "equality_eligible": true,\n "missing_sections": [],\n "status": "complete",\n "truncated_sections": []\n },\n "elapsed_ms": 10,\n "error": null,\n "event_sequence_end": 0,\n "event_sequence_start": 1,\n "status": "completed",\n "step_id": "open-settings",\n "step_index": 1\n }\n ]\n },\n "before_scenario": {\n "browser": {\n "cleanup": "disconnected-external",\n "mode": "connect",\n "process_ownership": "external",\n "product": "Chromium",\n "version": "149"\n },\n "completeness": {\n "equality_eligible": true,\n "missing_sections": [],\n "status": "complete",\n "truncated_sections": []\n },\n "duration_ms": 10,\n "events": {\n "dropped": 0,\n "items": [],\n "retained": 0\n },\n "limitations": [],\n "scenario": {\n "action_count": 1,\n "secret_references": [],\n "start_origin": "https://app.example.test"\n },\n "steps": [\n {\n "action": "scenario_start",\n "after_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "artifacts": {\n "accessibility": {\n "state": "not_requested"\n },\n "dom": {\n "state": "not_requested"\n },\n "history": {\n "state": "not_requested"\n },\n "screenshot": {\n "state": "not_requested"\n },\n "storage": {\n "state": "not_requested"\n },\n "url": {\n "state": "not_requested"\n }\n },\n "before_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "completeness": {\n "equality_eligible": true,\n "missing_sections": [],\n "status": "complete",\n "truncated_sections": []\n },\n "elapsed_ms": 0,\n "error": null,\n "event_sequence_end": 0,\n "event_sequence_start": 1,\n "status": "completed",\n "step_id": "scenario_start",\n "step_index": 0\n },\n {\n "action": "click",\n "after_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "artifacts": {\n "accessibility": {\n "state": "not_requested"\n },\n "dom": {\n "state": "not_requested"\n },\n "history": {\n "state": "not_requested"\n },\n "screenshot": {\n "state": "not_requested"\n },\n "storage": {\n "state": "not_requested"\n },\n "url": {\n "state": "not_requested"\n }\n },\n "before_url": {\n "origin": "https://app.example.test",\n "query_parameter_names": [],\n "redacted": false,\n "url": "https://app.example.test/"\n },\n "completeness": {\n "equality_eligible": true,\n "missing_sections": [],\n "status": "complete",\n "truncated_sections": []\n },\n "elapsed_ms": 10,\n "error": null,\n "event_sequence_end": 0,\n "event_sequence_start": 1,\n "status": "completed",\n "step_id": "open-settings",\n "step_index": 1\n }\n ]\n },\n "normalization": {\n "rules": [\n {\n "artifacts": [\n "dom",\n "accessibility"\n ],\n "match": "build-123",\n "replacement": "[BUILD_ID]",\n "rule_id": "volatile-build-id"\n }\n ]\n }\n }\n ],\n "properties": {\n "after": {\n "description": "Later passive web-page capture.",\n "properties": {\n "inspection": {\n "description": "Value for inspection.",\n "properties": {\n "accessibility": {\n "description": "Value for accessibility.",\n "properties": {\n "nodes": {\n "description": "Value for nodes.",\n "items": {\n "properties": {\n "description": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for description."\n },\n "ignored": {\n "description": "Value for ignored.",\n "type": "boolean"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact name used by this operation."\n },\n "node_id": {\n "description": "Value for node id.",\n "type": "string"\n },\n "parent_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for parent id."\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for role."\n },\n "states": {\n "default": [],\n "description": "Value for states.",\n "items": {\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "number"\n },\n {\n "type": "string"\n }\n ],\n "description": "Value for value."\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "parent_id",\n "role",\n "name",\n "description",\n "ignored"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "text_capture": {\n "description": "Value for text capture.",\n "properties": {\n "excluded_fields": {\n "description": "Value for excluded fields.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "retained_bytes": {\n "description": "Byte count for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "not_approved",\n "included",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "retained_bytes",\n "excluded_fields"\n ],\n "type": "object"\n },\n "total_nodes": {\n "description": "Value for total nodes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "text_capture",\n "nodes"\n ],\n "type": "object"\n },\n "browser": {\n "description": "Value for browser.",\n "properties": {\n "js_version": {\n "description": "Value for js version.",\n "type": "string"\n },\n "product": {\n "description": "Value for product.",\n "type": "string"\n },\n "protocol_version": {\n "description": "Value for protocol version.",\n "type": "string"\n },\n "revision": {\n "description": "Value for revision.",\n "type": "string"\n },\n "user_agent": {\n "description": "Value for user agent.",\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "capture_window": {\n "description": "Value for capture window.",\n "properties": {\n "ended_at": {\n "description": "Value for ended at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "observation_ms": {\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "started_at": {\n "description": "Value for started at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n }\n },\n "required": [\n "started_at",\n "ended_at",\n "observation_ms"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "properties": {\n "attach_limited_sections": {\n "description": "Value for attach limited sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "description": "Value for conditions.",\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "description": "Value for dropped events.",\n "properties": {\n "console_events": {\n "description": "Value for console events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "description": "Value for network requests.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "description": "Value for scripts.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "description": "Value for timeline events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "description": "Value for webmcp tools.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "description": "Value for websocket connections.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "description": "Value for websocket frames.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "description": "Value for excluded.",\n "items": {\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for count."\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "description": "Value for section.",\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "description": "Value for policy filtered sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "description": "Value for unavailable sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "console": {\n "description": "Value for console.",\n "properties": {\n "coverage_started_at": {\n "description": "Value for coverage started at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "events": {\n "description": "Value for events.",\n "items": {\n "properties": {\n "argument_types": {\n "description": "Value for argument types.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for column."\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for line."\n },\n "text_capture": {\n "description": "Value for text capture.",\n "properties": {\n "retained_bytes": {\n "description": "Byte count for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "not_approved",\n "included"\n ],\n "type": "string"\n },\n "values": {\n "description": "Value for values.",\n "items": {\n "properties": {\n "argument_index": {\n "description": "Value for argument index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n }\n },\n "required": [\n "argument_index",\n "type",\n "text"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "values",\n "retained_bytes"\n ],\n "type": "object"\n },\n "timestamp": {\n "description": "Value for timestamp.",\n "type": "number"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "type",\n "timestamp",\n "argument_types",\n "url",\n "line",\n "column",\n "text_capture"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "prior_activity_available": {\n "const": false,\n "description": "Value for prior activity available.",\n "type": "boolean"\n }\n },\n "required": [\n "events",\n "coverage_started_at",\n "prior_activity_available"\n ],\n "type": "object"\n },\n "dom": {\n "description": "Value for dom.",\n "properties": {\n "nodes": {\n "description": "Value for nodes.",\n "items": {\n "properties": {\n "attribute_names": {\n "description": "Value for attribute names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "index": {\n "description": "Value for index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_name": {\n "description": "Value for node name.",\n "type": "string"\n },\n "node_type": {\n "description": "Value for node type.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_value_length": {\n "description": "Value for node value length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parent_index": {\n "description": "Value for parent index.",\n "maximum": 9007199254740991,\n "minimum": -1,\n "type": "integer"\n }\n },\n "required": [\n "index",\n "parent_index",\n "node_type",\n "node_name",\n "node_value_length",\n "attribute_names"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total_nodes": {\n "description": "Value for total nodes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "nodes"\n ],\n "type": "object"\n },\n "frames": {\n "description": "Value for frames.",\n "items": {\n "properties": {\n "frame_id": {\n "description": "Value for frame id.",\n "type": "string"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for parent frame id."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "frame_id",\n "parent_frame_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "metadata": {\n "description": "Value for metadata.",\n "properties": {\n "agent_hints": {\n "description": "Value for agent hints.",\n "items": {\n "properties": {\n "declaration": {\n "description": "Value for declaration.",\n "type": "string"\n },\n "mechanism": {\n "description": "Value for mechanism.",\n "enum": [\n "link_rel",\n "dom_link_rel",\n "well_known_resource",\n "response_header"\n ],\n "type": "string"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "description": "Value for trust.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "mechanism",\n "declaration",\n "url",\n "trust"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "dom_urls": {\n "description": "Value for dom urls.",\n "items": {\n "properties": {\n "attribute": {\n "description": "Value for attribute.",\n "enum": [\n "href",\n "src",\n "action",\n "formaction",\n "poster"\n ],\n "type": "string"\n },\n "destination_scope": {\n "description": "Value for destination scope.",\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n "node_index": {\n "description": "Value for node index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "node_index",\n "attribute",\n "url",\n "destination_scope"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "excluded_dom_urls": {\n "description": "Value for excluded dom urls.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "headers_allowlisted": {\n "const": true,\n "description": "Value for headers allowlisted.",\n "type": "boolean"\n },\n "responses": {\n "description": "Value for responses.",\n "items": {\n "properties": {\n "content_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for content encoding."\n },\n "content_length": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for content length."\n },\n "csp": {\n "description": "Value for csp.",\n "properties": {\n "directives": {\n "description": "Value for directives.",\n "items": {\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "sources": {\n "description": "Value for sources.",\n "items": {\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "keyword",\n "scheme",\n "approved_origin",\n "external_origin",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for value."\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "sources"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "hash_count": {\n "description": "Value for hash count.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nonce_count": {\n "description": "Value for nonce count.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "directives",\n "nonce_count",\n "hash_count"\n ],\n "type": "object"\n },\n "links": {\n "description": "Value for links.",\n "items": {\n "properties": {\n "as": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for as."\n },\n "crossorigin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for crossorigin."\n },\n "destination_scope": {\n "description": "Value for destination scope.",\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n "href": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for href."\n },\n "rel": {\n "description": "Value for rel.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for type."\n }\n },\n "required": [\n "href",\n "destination_scope",\n "rel",\n "as",\n "type",\n "crossorigin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for mime type."\n },\n "policies": {\n "description": "Value for policies.",\n "properties": {\n "coep": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for coep."\n },\n "coop": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for coop."\n },\n "corp": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for corp."\n },\n "permissions_policy_features": {\n "description": "Value for permissions policy features.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "referrer_policy": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for referrer policy."\n },\n "x_content_type_options": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for x content type options."\n }\n },\n "required": [\n "coop",\n "coep",\n "corp",\n "referrer_policy",\n "x_content_type_options",\n "permissions_policy_features"\n ],\n "type": "object"\n },\n "request_id": {\n "description": "Value for request id.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "mime_type",\n "content_length",\n "content_encoding",\n "csp",\n "links",\n "policies"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "responses",\n "dom_urls",\n "agent_hints",\n "excluded_dom_urls",\n "headers_allowlisted"\n ],\n "type": "object"\n },\n "network": {\n "description": "Value for network.",\n "properties": {\n "coverage_started_at": {\n "description": "Value for coverage started at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "prior_activity_available": {\n "const": false,\n "description": "Value for prior activity available.",\n "type": "boolean"\n },\n "requests": {\n "description": "Value for requests.",\n "items": {\n "properties": {\n "body_shapes": {\n "description": "Value for body shapes.",\n "properties": {\n "request": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for request."\n },\n "response": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for response."\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "not_approved",\n "included",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "request",\n "response"\n ],\n "type": "object"\n },\n "encoded_data_length": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for encoded data length."\n },\n "initiator": {\n "description": "Value for initiator.",\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for column."\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for line."\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "type",\n "url",\n "line",\n "column"\n ],\n "type": "object"\n },\n "method": {\n "description": "Value for method.",\n "type": "string"\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for mime type."\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "request_id": {\n "description": "Value for request id.",\n "type": "string"\n },\n "resource_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for resource type."\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Current lifecycle or verification status."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "origin",\n "method",\n "resource_type",\n "status",\n "mime_type",\n "encoded_data_length",\n "initiator",\n "body_shapes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "websocket_events": {\n "description": "Value for websocket events.",\n "items": {\n "properties": {\n "direction": {\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "sent",\n "received"\n ],\n "type": "string"\n },\n "opcode": {\n "description": "Value for opcode.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_bytes": {\n "description": "Byte count for payload.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_shape": {\n "anyOf": [\n {\n "properties": {\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "json",\n "text",\n "binary"\n ],\n "type": "string"\n },\n "json_shape": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for json shape."\n }\n },\n "required": [\n "format",\n "json_shape"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for payload shape."\n },\n "request_id": {\n "description": "Value for request id.",\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "direction",\n "opcode",\n "payload_bytes",\n "payload_shape"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "requests",\n "websocket_events",\n "coverage_started_at",\n "prior_activity_available"\n ],\n "type": "object"\n },\n "resources": {\n "description": "Value for resources.",\n "items": {\n "properties": {\n "content_size": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for content size."\n },\n "mime_type": {\n "description": "Value for mime type.",\n "type": "string"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "resource_key": {\n "description": "Value for resource key.",\n "pattern": "^res_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "resource_key",\n "url",\n "origin",\n "type",\n "mime_type",\n "content_size"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "scripts": {\n "description": "Value for scripts.",\n "properties": {\n "items": {\n "description": "Value for items.",\n "items": {\n "properties": {\n "cdp_hash": {\n "description": "Value for cdp hash.",\n "type": "string"\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for frame id."\n },\n "is_module": {\n "description": "Whether is module.",\n "type": "boolean"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for language."\n },\n "length": {\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "resource_reconciliation": {\n "description": "Value for resource reconciliation.",\n "oneOf": [\n {\n "properties": {\n "resource_key": {\n "description": "Value for resource key.",\n "type": "string"\n },\n "status": {\n "const": "exact",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n }\n },\n "required": [\n "status",\n "resource_key"\n ],\n "type": "object"\n },\n {\n "properties": {\n "candidate_resource_keys": {\n "description": "Value for candidate resource keys.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "ambiguous",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n }\n },\n "required": [\n "status",\n "candidate_resource_keys"\n ],\n "type": "object"\n },\n {\n "properties": {\n "reason": {\n "const": "no_exact_sanitized_url",\n "description": "Value for reason.",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "script_key": {\n "description": "Value for script key.",\n "pattern": "^scr_[a-f0-9]{64}(?:_[0-9]+)?$",\n "type": "string"\n },\n "source": {\n "description": "Value for source.",\n "oneOf": [\n {\n "properties": {\n "included": {\n "const": false,\n "description": "Value for included.",\n "type": "boolean"\n },\n "reason": {\n "description": "Value for reason.",\n "type": "string"\n }\n },\n "required": [\n "included",\n "reason"\n ],\n "type": "object"\n },\n {\n "properties": {\n "artifact": {\n "description": "Value for artifact.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "description": "Value for charset.",\n "type": "string"\n },\n "media_type": {\n "description": "Value for media type.",\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n "included": {\n "const": true,\n "description": "Value for included.",\n "type": "boolean"\n }\n },\n "required": [\n "included",\n "artifact"\n ],\n "type": "object"\n }\n ]\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for source map url."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "url",\n "origin",\n "cdp_hash",\n "length",\n "is_module",\n "language",\n "source_map_url",\n "resource_reconciliation",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n },\n "storage": {\n "description": "Value for storage.",\n "properties": {\n "cache_names": {\n "description": "Value for cache names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "content_fingerprints": {\n "default": [],\n "description": "Value for content fingerprints.",\n "items": {\n "properties": {\n "complete": {\n "description": "Value for complete.",\n "type": "boolean"\n },\n "identity_sha256": {\n "description": "Exact SHA-256 digest of identity.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "description": "Value for scope.",\n "enum": [\n "cookie",\n "local_storage",\n "session_storage",\n "indexed_db_schema",\n "indexed_db_record",\n "cache_entry"\n ],\n "type": "string"\n },\n "value_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact SHA-256 digest of value."\n }\n },\n "required": [\n "scope",\n "identity_sha256",\n "value_sha256",\n "complete"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "fingerprint_algorithm": {\n "const": "sha256",\n "default": "sha256",\n "description": "Value for fingerprint algorithm.",\n "type": "string"\n },\n "fingerprints_complete": {\n "default": false,\n "description": "Value for fingerprints complete.",\n "type": "boolean"\n },\n "indexed_db_names": {\n "description": "Value for indexed db names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "local_storage_keys": {\n "description": "Value for local storage keys.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "origin": {\n "description": "Value for origin.",\n "type": "string"\n },\n "quota_bytes": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Byte count for quota."\n },\n "session_storage_keys": {\n "description": "Value for session storage keys.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "usage_bytes": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Byte count for usage."\n },\n "values_redacted": {\n "const": true,\n "description": "Value for values redacted.",\n "type": "boolean"\n }\n },\n "required": [\n "origin",\n "usage_bytes",\n "quota_bytes",\n "local_storage_keys",\n "session_storage_keys",\n "indexed_db_names",\n "cache_names",\n "values_redacted"\n ],\n "type": "object"\n },\n "target": {\n "description": "Value for target.",\n "properties": {\n "attached": {\n "description": "Value for attached.",\n "type": "boolean"\n },\n "origin": {\n "description": "Value for origin.",\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "type": "string"\n },\n "title": {\n "description": "Value for title.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "title",\n "url",\n "origin",\n "attached"\n ],\n "type": "object"\n },\n "workers": {\n "description": "Value for workers.",\n "items": {\n "properties": {\n "attached": {\n "description": "Value for attached.",\n "type": "boolean"\n },\n "opener_target_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for opener target id."\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for parent frame id."\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "url",\n "origin",\n "attached"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "browser",\n "target",\n "capture_window",\n "completeness",\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "resources",\n "network",\n "console",\n "workers",\n "metadata",\n "storage",\n "limitations"\n ],\n "type": "object"\n },\n "webmcp": {\n "anyOf": [\n {\n "properties": {\n "browser": {\n "description": "Value for browser.",\n "properties": {\n "js_version": {\n "description": "Value for js version.",\n "type": "string"\n },\n "product": {\n "description": "Value for product.",\n "type": "string"\n },\n "protocol_version": {\n "description": "Value for protocol version.",\n "type": "string"\n },\n "revision": {\n "description": "Value for revision.",\n "type": "string"\n },\n "user_agent": {\n "description": "Value for user agent.",\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "properties": {\n "attach_limited_sections": {\n "description": "Value for attach limited sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "description": "Value for conditions.",\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "description": "Value for dropped events.",\n "properties": {\n "console_events": {\n "description": "Value for console events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "description": "Value for network requests.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "description": "Value for scripts.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "description": "Value for timeline events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "description": "Value for webmcp tools.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "description": "Value for websocket connections.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "description": "Value for websocket frames.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "description": "Value for excluded.",\n "items": {\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for count."\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "description": "Value for section.",\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "description": "Value for policy filtered sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "description": "Value for unavailable sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "available",\n "unavailable"\n ],\n "type": "string"\n },\n "target": {\n "description": "Value for target.",\n "properties": {\n "origin": {\n "description": "Value for origin.",\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "tools": {\n "description": "Value for tools.",\n "properties": {\n "items": {\n "description": "Value for items.",\n "items": {\n "properties": {\n "annotations": {\n "description": "Value for annotations.",\n "properties": {\n "autosubmit": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for autosubmit."\n },\n "read_only": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for read only."\n },\n "untrusted_content": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for untrusted content."\n }\n },\n "required": [\n "read_only",\n "untrusted_content",\n "autosubmit"\n ],\n "type": "object"\n },\n "declaration_kind": {\n "description": "Value for declaration kind.",\n "enum": [\n "declarative",\n "imperative"\n ],\n "type": "string"\n },\n "description": {\n "description": "Value for description.",\n "type": "string"\n },\n "frame_id": {\n "description": "Value for frame id.",\n "type": "string"\n },\n "frame_url": {\n "description": "Value for frame url.",\n "type": "string"\n },\n "input_schema_shape": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for input schema shape."\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "owner_origin": {\n "description": "Value for owner origin.",\n "type": "string"\n },\n "registration_source": {\n "anyOf": [\n {\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for column."\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for line."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "line",\n "column"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for registration source."\n },\n "tool_key": {\n "description": "Value for tool key.",\n "pattern": "^webmcp_[a-f0-9]{64}$",\n "type": "string"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "description": "Value for trust.",\n "type": "string"\n }\n },\n "required": [\n "tool_key",\n "name",\n "description",\n "frame_id",\n "frame_url",\n "owner_origin",\n "declaration_kind",\n "input_schema_shape",\n "annotations",\n "registration_source",\n "trust"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "status",\n "tools",\n "completeness",\n "limitations"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for webmcp."\n }\n },\n "required": [\n "inspection"\n ],\n "type": "object"\n },\n "after_scenario": {\n "additionalProperties": false,\n "description": "Later browser scenario capture.",\n "properties": {\n "browser": {\n "additionalProperties": false,\n "description": "Value for browser.",\n "properties": {\n "cleanup": {\n "description": "Value for cleanup.",\n "enum": [\n "terminated-owned-process",\n "disconnected-external"\n ],\n "type": "string"\n },\n "mode": {\n "description": "Operation mode that selects the requested behavior.",\n "enum": [\n "launch",\n "connect"\n ],\n "type": "string"\n },\n "process_ownership": {\n "description": "Value for process ownership.",\n "enum": [\n "provider-owned",\n "external"\n ],\n "type": "string"\n },\n "product": {\n "description": "Value for product.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mode",\n "process_ownership",\n "cleanup",\n "product",\n "version"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "duration_ms": {\n "description": "Value for duration ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "events": {\n "additionalProperties": false,\n "description": "Value for events.",\n "properties": {\n "dropped": {\n "description": "Value for dropped.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "items": {\n "description": "Value for items.",\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "console",\n "description": "Value for kind.",\n "type": "string"\n },\n "level": {\n "description": "Value for level.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "level",\n "text",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "page-error",\n "description": "Value for kind.",\n "type": "string"\n },\n "message": {\n "description": "Value for message.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "stack": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for stack."\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "message",\n "stack"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "response",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "maximum": 599,\n "minimum": 100,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "minLength": 1,\n "type": "string"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-failed",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-finished",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-unfinished",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "capture-ended",\n "cancelled"\n ],\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "body": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_exposed",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "description": "Value for message.",\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "request-failed",\n "response-unfinished",\n "body-read-failed",\n "body-read-timeout",\n "cancelled",\n "capture-ended"\n ],\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "content": {\n "description": "Value for content.",\n "type": "string"\n },\n "encoding": {\n "const": "base64",\n "description": "Value for encoding.",\n "type": "string"\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for media type."\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "representation": {\n "description": "Value for representation.",\n "enum": [\n "browser-exposed-request-bytes",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "representation",\n "encoding",\n "content",\n "bytes",\n "sha256",\n "media_type",\n "redacted"\n ],\n "type": "object"\n }\n ],\n "description": "Value for body."\n },\n "headers": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "description": "Value for message.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "message"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "items": {\n "description": "Value for items.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for value."\n }\n },\n "required": [\n "name",\n "value",\n "redacted"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "items"\n ],\n "type": "object"\n }\n ],\n "description": "Value for headers."\n },\n "kind": {\n "const": "network-content",\n "description": "Value for kind.",\n "type": "string"\n },\n "phase": {\n "description": "Current plan or execution phase of the operation.",\n "enum": [\n "request",\n "response"\n ],\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source_event_sequence": {\n "description": "Value for source event sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "transaction_id",\n "source_event_sequence",\n "phase",\n "headers",\n "body"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "websocket-opened",\n "websocket-closed"\n ],\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "websocket-frame-sent",\n "websocket-frame-received"\n ],\n "type": "string"\n },\n "payload_bytes": {\n "description": "Byte count for payload.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_text": {\n "description": "Value for payload text.",\n "type": "string"\n },\n "payload_type": {\n "const": "text",\n "description": "Value for payload type.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "description": "Value for truncated.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "payload_bytes",\n "truncated",\n "payload_type",\n "payload_text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "websocket-frame-sent",\n "websocket-frame-received"\n ],\n "type": "string"\n },\n "payload_bytes": {\n "description": "Byte count for payload.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_text": {\n "description": "Value for payload text.",\n "type": "null"\n },\n "payload_type": {\n "const": "binary",\n "description": "Value for payload type.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "description": "Value for truncated.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "payload_bytes",\n "truncated",\n "payload_type",\n "payload_text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "frame-attached",\n "frame-detached",\n "frame-navigated",\n "worker-created",\n "worker-closed",\n "popup-opened",\n "popup-closed"\n ],\n "type": "string"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact name used by this operation."\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "anyOf": [\n {\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "download-cancelled",\n "description": "Value for kind.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "suggested_filename": {\n "description": "Value for suggested filename.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "suggested_filename",\n "url"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "retained",\n "dropped",\n "items"\n ],\n "type": "object"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "scenario": {\n "additionalProperties": false,\n "description": "Value for scenario.",\n "properties": {\n "action_count": {\n "description": "Value for action count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "network_content": {\n "additionalProperties": false,\n "description": "Value for network content.",\n "properties": {\n "header_values": {\n "default": false,\n "description": "Value for header values.",\n "type": "boolean"\n },\n "request_body": {\n "default": false,\n "description": "Value for request body.",\n "type": "boolean"\n },\n "response_body": {\n "default": false,\n "description": "Value for response body.",\n "type": "boolean"\n }\n },\n "type": "object"\n },\n "secret_references": {\n "description": "Value for secret references.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "start_origin": {\n "description": "Value for start origin.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "start_origin",\n "action_count",\n "secret_references"\n ],\n "type": "object"\n },\n "steps": {\n "description": "Value for steps.",\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "description": "Value for action.",\n "minLength": 1,\n "type": "string"\n },\n "after_url": {\n "description": "Value for after url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "artifacts": {\n "additionalProperties": false,\n "description": "Value for artifacts.",\n "properties": {\n "accessibility": {\n "description": "Value for accessibility.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "dom": {\n "description": "Value for dom.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "history": {\n "description": "Value for history.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "current_url": {\n "description": "Value for current url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "length": {\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "navigation_entries": {\n "description": "Value for navigation entries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "type": {\n "description": "Value for type.",\n "enum": [\n "navigate",\n "reload",\n "back_forward",\n "prerender",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "type",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "length",\n "current_url",\n "navigation_entries"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "screenshot": {\n "description": "Value for screenshot.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "description": "Value for data base64.",\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "description": "Value for media type.",\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "description": "Value for storage.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "cookies": {\n "description": "Value for cookies.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "local_storage": {\n "description": "Value for local storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "session_storage": {\n "description": "Value for session storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "cookies",\n "local_storage",\n "session_storage"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "description": "Value for url.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "object"\n },\n "before_url": {\n "description": "Value for before url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "elapsed_ms": {\n "description": "Value for elapsed ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "description": "Structured, caller-actionable error when the operation fails.",\n "type": "null"\n },\n "event_sequence_end": {\n "description": "Value for event sequence end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "event_sequence_start": {\n "description": "Value for event sequence start.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "const": "completed",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_index",\n "step_id",\n "action",\n "elapsed_ms",\n "before_url",\n "after_url",\n "event_sequence_start",\n "event_sequence_end",\n "artifacts",\n "completeness",\n "status",\n "error"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "description": "Value for action.",\n "minLength": 1,\n "type": "string"\n },\n "after_url": {\n "description": "Value for after url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "artifacts": {\n "additionalProperties": false,\n "description": "Value for artifacts.",\n "properties": {\n "accessibility": {\n "description": "Value for accessibility.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "dom": {\n "description": "Value for dom.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "history": {\n "description": "Value for history.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "current_url": {\n "description": "Value for current url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "length": {\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "navigation_entries": {\n "description": "Value for navigation entries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "type": {\n "description": "Value for type.",\n "enum": [\n "navigate",\n "reload",\n "back_forward",\n "prerender",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "type",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "length",\n "current_url",\n "navigation_entries"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "screenshot": {\n "description": "Value for screenshot.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "description": "Value for data base64.",\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "description": "Value for media type.",\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "description": "Value for storage.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "cookies": {\n "description": "Value for cookies.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "local_storage": {\n "description": "Value for local storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "session_storage": {\n "description": "Value for session storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "cookies",\n "local_storage",\n "session_storage"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "description": "Value for url.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "object"\n },\n "before_url": {\n "description": "Value for before url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "elapsed_ms": {\n "description": "Value for elapsed ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "description": "Structured, caller-actionable error when the operation fails.",\n "type": "string"\n },\n "event_sequence_end": {\n "description": "Value for event sequence end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "event_sequence_start": {\n "description": "Value for event sequence start.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "failed",\n "cancelled"\n ],\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_index",\n "step_id",\n "action",\n "elapsed_ms",\n "before_url",\n "after_url",\n "event_sequence_start",\n "event_sequence_end",\n "artifacts",\n "completeness",\n "status",\n "error"\n ],\n "type": "object"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "browser",\n "scenario",\n "duration_ms",\n "steps",\n "events",\n "completeness",\n "limitations"\n ],\n "type": "object"\n },\n "before": {\n "description": "Earlier passive web-page capture.",\n "properties": {\n "inspection": {\n "description": "Value for inspection.",\n "properties": {\n "accessibility": {\n "description": "Value for accessibility.",\n "properties": {\n "nodes": {\n "description": "Value for nodes.",\n "items": {\n "properties": {\n "description": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for description."\n },\n "ignored": {\n "description": "Value for ignored.",\n "type": "boolean"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact name used by this operation."\n },\n "node_id": {\n "description": "Value for node id.",\n "type": "string"\n },\n "parent_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for parent id."\n },\n "role": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for role."\n },\n "states": {\n "default": [],\n "description": "Value for states.",\n "items": {\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "number"\n },\n {\n "type": "string"\n }\n ],\n "description": "Value for value."\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "parent_id",\n "role",\n "name",\n "description",\n "ignored"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "text_capture": {\n "description": "Value for text capture.",\n "properties": {\n "excluded_fields": {\n "description": "Value for excluded fields.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "retained_bytes": {\n "description": "Byte count for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "not_approved",\n "included",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "retained_bytes",\n "excluded_fields"\n ],\n "type": "object"\n },\n "total_nodes": {\n "description": "Value for total nodes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "text_capture",\n "nodes"\n ],\n "type": "object"\n },\n "browser": {\n "description": "Value for browser.",\n "properties": {\n "js_version": {\n "description": "Value for js version.",\n "type": "string"\n },\n "product": {\n "description": "Value for product.",\n "type": "string"\n },\n "protocol_version": {\n "description": "Value for protocol version.",\n "type": "string"\n },\n "revision": {\n "description": "Value for revision.",\n "type": "string"\n },\n "user_agent": {\n "description": "Value for user agent.",\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "capture_window": {\n "description": "Value for capture window.",\n "properties": {\n "ended_at": {\n "description": "Value for ended at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "observation_ms": {\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "started_at": {\n "description": "Value for started at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n }\n },\n "required": [\n "started_at",\n "ended_at",\n "observation_ms"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "properties": {\n "attach_limited_sections": {\n "description": "Value for attach limited sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "description": "Value for conditions.",\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "description": "Value for dropped events.",\n "properties": {\n "console_events": {\n "description": "Value for console events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "description": "Value for network requests.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "description": "Value for scripts.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "description": "Value for timeline events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "description": "Value for webmcp tools.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "description": "Value for websocket connections.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "description": "Value for websocket frames.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "description": "Value for excluded.",\n "items": {\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for count."\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "description": "Value for section.",\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "description": "Value for policy filtered sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "description": "Value for unavailable sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "console": {\n "description": "Value for console.",\n "properties": {\n "coverage_started_at": {\n "description": "Value for coverage started at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "events": {\n "description": "Value for events.",\n "items": {\n "properties": {\n "argument_types": {\n "description": "Value for argument types.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for column."\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for line."\n },\n "text_capture": {\n "description": "Value for text capture.",\n "properties": {\n "retained_bytes": {\n "description": "Byte count for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "not_approved",\n "included"\n ],\n "type": "string"\n },\n "values": {\n "description": "Value for values.",\n "items": {\n "properties": {\n "argument_index": {\n "description": "Value for argument index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n }\n },\n "required": [\n "argument_index",\n "type",\n "text"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "values",\n "retained_bytes"\n ],\n "type": "object"\n },\n "timestamp": {\n "description": "Value for timestamp.",\n "type": "number"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "type",\n "timestamp",\n "argument_types",\n "url",\n "line",\n "column",\n "text_capture"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "prior_activity_available": {\n "const": false,\n "description": "Value for prior activity available.",\n "type": "boolean"\n }\n },\n "required": [\n "events",\n "coverage_started_at",\n "prior_activity_available"\n ],\n "type": "object"\n },\n "dom": {\n "description": "Value for dom.",\n "properties": {\n "nodes": {\n "description": "Value for nodes.",\n "items": {\n "properties": {\n "attribute_names": {\n "description": "Value for attribute names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "index": {\n "description": "Value for index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_name": {\n "description": "Value for node name.",\n "type": "string"\n },\n "node_type": {\n "description": "Value for node type.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_value_length": {\n "description": "Value for node value length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parent_index": {\n "description": "Value for parent index.",\n "maximum": 9007199254740991,\n "minimum": -1,\n "type": "integer"\n }\n },\n "required": [\n "index",\n "parent_index",\n "node_type",\n "node_name",\n "node_value_length",\n "attribute_names"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total_nodes": {\n "description": "Value for total nodes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "nodes"\n ],\n "type": "object"\n },\n "frames": {\n "description": "Value for frames.",\n "items": {\n "properties": {\n "frame_id": {\n "description": "Value for frame id.",\n "type": "string"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for parent frame id."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "frame_id",\n "parent_frame_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "metadata": {\n "description": "Value for metadata.",\n "properties": {\n "agent_hints": {\n "description": "Value for agent hints.",\n "items": {\n "properties": {\n "declaration": {\n "description": "Value for declaration.",\n "type": "string"\n },\n "mechanism": {\n "description": "Value for mechanism.",\n "enum": [\n "link_rel",\n "dom_link_rel",\n "well_known_resource",\n "response_header"\n ],\n "type": "string"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "description": "Value for trust.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "mechanism",\n "declaration",\n "url",\n "trust"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "dom_urls": {\n "description": "Value for dom urls.",\n "items": {\n "properties": {\n "attribute": {\n "description": "Value for attribute.",\n "enum": [\n "href",\n "src",\n "action",\n "formaction",\n "poster"\n ],\n "type": "string"\n },\n "destination_scope": {\n "description": "Value for destination scope.",\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n "node_index": {\n "description": "Value for node index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "node_index",\n "attribute",\n "url",\n "destination_scope"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "excluded_dom_urls": {\n "description": "Value for excluded dom urls.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "headers_allowlisted": {\n "const": true,\n "description": "Value for headers allowlisted.",\n "type": "boolean"\n },\n "responses": {\n "description": "Value for responses.",\n "items": {\n "properties": {\n "content_encoding": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for content encoding."\n },\n "content_length": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for content length."\n },\n "csp": {\n "description": "Value for csp.",\n "properties": {\n "directives": {\n "description": "Value for directives.",\n "items": {\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "sources": {\n "description": "Value for sources.",\n "items": {\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "keyword",\n "scheme",\n "approved_origin",\n "external_origin",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for value."\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "sources"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "hash_count": {\n "description": "Value for hash count.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nonce_count": {\n "description": "Value for nonce count.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "directives",\n "nonce_count",\n "hash_count"\n ],\n "type": "object"\n },\n "links": {\n "description": "Value for links.",\n "items": {\n "properties": {\n "as": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for as."\n },\n "crossorigin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for crossorigin."\n },\n "destination_scope": {\n "description": "Value for destination scope.",\n "enum": [\n "approved",\n "outside_policy",\n "unsupported"\n ],\n "type": "string"\n },\n "href": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for href."\n },\n "rel": {\n "description": "Value for rel.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for type."\n }\n },\n "required": [\n "href",\n "destination_scope",\n "rel",\n "as",\n "type",\n "crossorigin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for mime type."\n },\n "policies": {\n "description": "Value for policies.",\n "properties": {\n "coep": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for coep."\n },\n "coop": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for coop."\n },\n "corp": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for corp."\n },\n "permissions_policy_features": {\n "description": "Value for permissions policy features.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "referrer_policy": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for referrer policy."\n },\n "x_content_type_options": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for x content type options."\n }\n },\n "required": [\n "coop",\n "coep",\n "corp",\n "referrer_policy",\n "x_content_type_options",\n "permissions_policy_features"\n ],\n "type": "object"\n },\n "request_id": {\n "description": "Value for request id.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "mime_type",\n "content_length",\n "content_encoding",\n "csp",\n "links",\n "policies"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "responses",\n "dom_urls",\n "agent_hints",\n "excluded_dom_urls",\n "headers_allowlisted"\n ],\n "type": "object"\n },\n "network": {\n "description": "Value for network.",\n "properties": {\n "coverage_started_at": {\n "description": "Value for coverage started at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "prior_activity_available": {\n "const": false,\n "description": "Value for prior activity available.",\n "type": "boolean"\n },\n "requests": {\n "description": "Value for requests.",\n "items": {\n "properties": {\n "body_shapes": {\n "description": "Value for body shapes.",\n "properties": {\n "request": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for request."\n },\n "response": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for response."\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "not_approved",\n "included",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "request",\n "response"\n ],\n "type": "object"\n },\n "encoded_data_length": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for encoded data length."\n },\n "initiator": {\n "description": "Value for initiator.",\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for column."\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for line."\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "type",\n "url",\n "line",\n "column"\n ],\n "type": "object"\n },\n "method": {\n "description": "Value for method.",\n "type": "string"\n },\n "mime_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for mime type."\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "request_id": {\n "description": "Value for request id.",\n "type": "string"\n },\n "resource_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for resource type."\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Current lifecycle or verification status."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "origin",\n "method",\n "resource_type",\n "status",\n "mime_type",\n "encoded_data_length",\n "initiator",\n "body_shapes"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "websocket_events": {\n "description": "Value for websocket events.",\n "items": {\n "properties": {\n "direction": {\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "sent",\n "received"\n ],\n "type": "string"\n },\n "opcode": {\n "description": "Value for opcode.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_bytes": {\n "description": "Byte count for payload.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_shape": {\n "anyOf": [\n {\n "properties": {\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "json",\n "text",\n "binary"\n ],\n "type": "string"\n },\n "json_shape": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for json shape."\n }\n },\n "required": [\n "format",\n "json_shape"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for payload shape."\n },\n "request_id": {\n "description": "Value for request id.",\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "direction",\n "opcode",\n "payload_bytes",\n "payload_shape"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "requests",\n "websocket_events",\n "coverage_started_at",\n "prior_activity_available"\n ],\n "type": "object"\n },\n "resources": {\n "description": "Value for resources.",\n "items": {\n "properties": {\n "content_size": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for content size."\n },\n "mime_type": {\n "description": "Value for mime type.",\n "type": "string"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "resource_key": {\n "description": "Value for resource key.",\n "pattern": "^res_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "resource_key",\n "url",\n "origin",\n "type",\n "mime_type",\n "content_size"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "scripts": {\n "description": "Value for scripts.",\n "properties": {\n "items": {\n "description": "Value for items.",\n "items": {\n "properties": {\n "cdp_hash": {\n "description": "Value for cdp hash.",\n "type": "string"\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for frame id."\n },\n "is_module": {\n "description": "Whether is module.",\n "type": "boolean"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for language."\n },\n "length": {\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "resource_reconciliation": {\n "description": "Value for resource reconciliation.",\n "oneOf": [\n {\n "properties": {\n "resource_key": {\n "description": "Value for resource key.",\n "type": "string"\n },\n "status": {\n "const": "exact",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n }\n },\n "required": [\n "status",\n "resource_key"\n ],\n "type": "object"\n },\n {\n "properties": {\n "candidate_resource_keys": {\n "description": "Value for candidate resource keys.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "ambiguous",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n }\n },\n "required": [\n "status",\n "candidate_resource_keys"\n ],\n "type": "object"\n },\n {\n "properties": {\n "reason": {\n "const": "no_exact_sanitized_url",\n "description": "Value for reason.",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "script_key": {\n "description": "Value for script key.",\n "pattern": "^scr_[a-f0-9]{64}(?:_[0-9]+)?$",\n "type": "string"\n },\n "source": {\n "description": "Value for source.",\n "oneOf": [\n {\n "properties": {\n "included": {\n "const": false,\n "description": "Value for included.",\n "type": "boolean"\n },\n "reason": {\n "description": "Value for reason.",\n "type": "string"\n }\n },\n "required": [\n "included",\n "reason"\n ],\n "type": "object"\n },\n {\n "properties": {\n "artifact": {\n "description": "Value for artifact.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "description": "Value for charset.",\n "type": "string"\n },\n "media_type": {\n "description": "Value for media type.",\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n "included": {\n "const": true,\n "description": "Value for included.",\n "type": "boolean"\n }\n },\n "required": [\n "included",\n "artifact"\n ],\n "type": "object"\n }\n ]\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for source map url."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "url",\n "origin",\n "cdp_hash",\n "length",\n "is_module",\n "language",\n "source_map_url",\n "resource_reconciliation",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n },\n "storage": {\n "description": "Value for storage.",\n "properties": {\n "cache_names": {\n "description": "Value for cache names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "content_fingerprints": {\n "default": [],\n "description": "Value for content fingerprints.",\n "items": {\n "properties": {\n "complete": {\n "description": "Value for complete.",\n "type": "boolean"\n },\n "identity_sha256": {\n "description": "Exact SHA-256 digest of identity.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "description": "Value for scope.",\n "enum": [\n "cookie",\n "local_storage",\n "session_storage",\n "indexed_db_schema",\n "indexed_db_record",\n "cache_entry"\n ],\n "type": "string"\n },\n "value_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact SHA-256 digest of value."\n }\n },\n "required": [\n "scope",\n "identity_sha256",\n "value_sha256",\n "complete"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "fingerprint_algorithm": {\n "const": "sha256",\n "default": "sha256",\n "description": "Value for fingerprint algorithm.",\n "type": "string"\n },\n "fingerprints_complete": {\n "default": false,\n "description": "Value for fingerprints complete.",\n "type": "boolean"\n },\n "indexed_db_names": {\n "description": "Value for indexed db names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "local_storage_keys": {\n "description": "Value for local storage keys.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "origin": {\n "description": "Value for origin.",\n "type": "string"\n },\n "quota_bytes": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Byte count for quota."\n },\n "session_storage_keys": {\n "description": "Value for session storage keys.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "usage_bytes": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ],\n "description": "Byte count for usage."\n },\n "values_redacted": {\n "const": true,\n "description": "Value for values redacted.",\n "type": "boolean"\n }\n },\n "required": [\n "origin",\n "usage_bytes",\n "quota_bytes",\n "local_storage_keys",\n "session_storage_keys",\n "indexed_db_names",\n "cache_names",\n "values_redacted"\n ],\n "type": "object"\n },\n "target": {\n "description": "Value for target.",\n "properties": {\n "attached": {\n "description": "Value for attached.",\n "type": "boolean"\n },\n "origin": {\n "description": "Value for origin.",\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "type": "string"\n },\n "title": {\n "description": "Value for title.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "title",\n "url",\n "origin",\n "attached"\n ],\n "type": "object"\n },\n "workers": {\n "description": "Value for workers.",\n "items": {\n "properties": {\n "attached": {\n "description": "Value for attached.",\n "type": "boolean"\n },\n "opener_target_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for opener target id."\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for parent frame id."\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "type": "string"\n },\n "type": {\n "description": "Value for type.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "url",\n "origin",\n "attached"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "browser",\n "target",\n "capture_window",\n "completeness",\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "resources",\n "network",\n "console",\n "workers",\n "metadata",\n "storage",\n "limitations"\n ],\n "type": "object"\n },\n "webmcp": {\n "anyOf": [\n {\n "properties": {\n "browser": {\n "description": "Value for browser.",\n "properties": {\n "js_version": {\n "description": "Value for js version.",\n "type": "string"\n },\n "product": {\n "description": "Value for product.",\n "type": "string"\n },\n "protocol_version": {\n "description": "Value for protocol version.",\n "type": "string"\n },\n "revision": {\n "description": "Value for revision.",\n "type": "string"\n },\n "user_agent": {\n "description": "Value for user agent.",\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "properties": {\n "attach_limited_sections": {\n "description": "Value for attach limited sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "description": "Value for conditions.",\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "description": "Value for dropped events.",\n "properties": {\n "console_events": {\n "description": "Value for console events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "description": "Value for network requests.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "description": "Value for scripts.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "description": "Value for timeline events.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "description": "Value for webmcp tools.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "description": "Value for websocket connections.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "description": "Value for websocket frames.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "description": "Value for excluded.",\n "items": {\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for count."\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "description": "Value for section.",\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "description": "Value for policy filtered sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "description": "Value for unavailable sections.",\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "available",\n "unavailable"\n ],\n "type": "string"\n },\n "target": {\n "description": "Value for target.",\n "properties": {\n "origin": {\n "description": "Value for origin.",\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "tools": {\n "description": "Value for tools.",\n "properties": {\n "items": {\n "description": "Value for items.",\n "items": {\n "properties": {\n "annotations": {\n "description": "Value for annotations.",\n "properties": {\n "autosubmit": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for autosubmit."\n },\n "read_only": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for read only."\n },\n "untrusted_content": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for untrusted content."\n }\n },\n "required": [\n "read_only",\n "untrusted_content",\n "autosubmit"\n ],\n "type": "object"\n },\n "declaration_kind": {\n "description": "Value for declaration kind.",\n "enum": [\n "declarative",\n "imperative"\n ],\n "type": "string"\n },\n "description": {\n "description": "Value for description.",\n "type": "string"\n },\n "frame_id": {\n "description": "Value for frame id.",\n "type": "string"\n },\n "frame_url": {\n "description": "Value for frame url.",\n "type": "string"\n },\n "input_schema_shape": {\n "anyOf": [\n {\n "properties": {\n "max_depth_observed": {\n "description": "Maximum permitted depth observed for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_count": {\n "description": "Value for node count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "properties": {\n "description": "Value for properties.",\n "items": {\n "properties": {\n "observations": {\n "description": "Value for observations.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "types": {\n "description": "Value for types.",\n "items": {\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "path",\n "types",\n "observations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_type": {\n "description": "Value for root type.",\n "enum": [\n "object",\n "array",\n "string",\n "number",\n "boolean",\n "null"\n ],\n "type": "string"\n }\n },\n "required": [\n "root_type",\n "node_count",\n "max_depth_observed",\n "properties"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for input schema shape."\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "owner_origin": {\n "description": "Value for owner origin.",\n "type": "string"\n },\n "registration_source": {\n "anyOf": [\n {\n "properties": {\n "column": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for column."\n },\n "line": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for line."\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "line",\n "column"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for registration source."\n },\n "tool_key": {\n "description": "Value for tool key.",\n "pattern": "^webmcp_[a-f0-9]{64}$",\n "type": "string"\n },\n "trust": {\n "const": "page-declared-untrusted",\n "description": "Value for trust.",\n "type": "string"\n }\n },\n "required": [\n "tool_key",\n "name",\n "description",\n "frame_id",\n "frame_url",\n "owner_origin",\n "declaration_kind",\n "input_schema_shape",\n "annotations",\n "registration_source",\n "trust"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "description": "Value for total.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "status",\n "tools",\n "completeness",\n "limitations"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for webmcp."\n }\n },\n "required": [\n "inspection"\n ],\n "type": "object"\n },\n "before_scenario": {\n "additionalProperties": false,\n "description": "Earlier browser scenario capture.",\n "properties": {\n "browser": {\n "additionalProperties": false,\n "description": "Value for browser.",\n "properties": {\n "cleanup": {\n "description": "Value for cleanup.",\n "enum": [\n "terminated-owned-process",\n "disconnected-external"\n ],\n "type": "string"\n },\n "mode": {\n "description": "Operation mode that selects the requested behavior.",\n "enum": [\n "launch",\n "connect"\n ],\n "type": "string"\n },\n "process_ownership": {\n "description": "Value for process ownership.",\n "enum": [\n "provider-owned",\n "external"\n ],\n "type": "string"\n },\n "product": {\n "description": "Value for product.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mode",\n "process_ownership",\n "cleanup",\n "product",\n "version"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "duration_ms": {\n "description": "Value for duration ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "events": {\n "additionalProperties": false,\n "description": "Value for events.",\n "properties": {\n "dropped": {\n "description": "Value for dropped.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "items": {\n "description": "Value for items.",\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "console",\n "description": "Value for kind.",\n "type": "string"\n },\n "level": {\n "description": "Value for level.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "level",\n "text",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "page-error",\n "description": "Value for kind.",\n "type": "string"\n },\n "message": {\n "description": "Value for message.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "stack": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for stack."\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "message",\n "stack"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "response",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "maximum": 599,\n "minimum": 100,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "minLength": 1,\n "type": "string"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-failed",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-finished",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "description": "Value for failure.",\n "type": "null"\n },\n "header_names": {\n "description": "Value for header names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-unfinished",\n "description": "Value for kind.",\n "type": "string"\n },\n "method": {\n "description": "Value for method.",\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "capture-ended",\n "cancelled"\n ],\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for redirected from transaction id."\n },\n "resource_type": {\n "description": "Value for resource type.",\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "type": "null"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "body": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_exposed",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "description": "Value for message.",\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "request-failed",\n "response-unfinished",\n "body-read-failed",\n "body-read-timeout",\n "cancelled",\n "capture-ended"\n ],\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "content": {\n "description": "Value for content.",\n "type": "string"\n },\n "encoding": {\n "const": "base64",\n "description": "Value for encoding.",\n "type": "string"\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for media type."\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "representation": {\n "description": "Value for representation.",\n "enum": [\n "browser-exposed-request-bytes",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "representation",\n "encoding",\n "content",\n "bytes",\n "sha256",\n "media_type",\n "redacted"\n ],\n "type": "object"\n }\n ],\n "description": "Value for body."\n },\n "headers": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "description": "Value for message.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "message"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "items": {\n "description": "Value for items.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for value."\n }\n },\n "required": [\n "name",\n "value",\n "redacted"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "items"\n ],\n "type": "object"\n }\n ],\n "description": "Value for headers."\n },\n "kind": {\n "const": "network-content",\n "description": "Value for kind.",\n "type": "string"\n },\n "phase": {\n "description": "Current plan or execution phase of the operation.",\n "enum": [\n "request",\n "response"\n ],\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source_event_sequence": {\n "description": "Value for source event sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "description": "Value for transaction id.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "transaction_id",\n "source_event_sequence",\n "phase",\n "headers",\n "body"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "websocket-opened",\n "websocket-closed"\n ],\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "websocket-frame-sent",\n "websocket-frame-received"\n ],\n "type": "string"\n },\n "payload_bytes": {\n "description": "Byte count for payload.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_text": {\n "description": "Value for payload text.",\n "type": "string"\n },\n "payload_type": {\n "const": "text",\n "description": "Value for payload type.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "description": "Value for truncated.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "payload_bytes",\n "truncated",\n "payload_type",\n "payload_text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "websocket-frame-sent",\n "websocket-frame-received"\n ],\n "type": "string"\n },\n "payload_bytes": {\n "description": "Byte count for payload.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_text": {\n "description": "Value for payload text.",\n "type": "null"\n },\n "payload_type": {\n "const": "binary",\n "description": "Value for payload type.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "description": "Value for truncated.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "payload_bytes",\n "truncated",\n "payload_type",\n "payload_text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "frame-attached",\n "frame-detached",\n "frame-navigated",\n "worker-created",\n "worker-closed",\n "popup-opened",\n "popup-closed"\n ],\n "type": "string"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact name used by this operation."\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "anyOf": [\n {\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for url."\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "download-cancelled",\n "description": "Value for kind.",\n "type": "string"\n },\n "sequence": {\n "description": "Value for sequence.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "suggested_filename": {\n "description": "Value for suggested filename.",\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "suggested_filename",\n "url"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "retained",\n "dropped",\n "items"\n ],\n "type": "object"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "scenario": {\n "additionalProperties": false,\n "description": "Value for scenario.",\n "properties": {\n "action_count": {\n "description": "Value for action count.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "network_content": {\n "additionalProperties": false,\n "description": "Value for network content.",\n "properties": {\n "header_values": {\n "default": false,\n "description": "Value for header values.",\n "type": "boolean"\n },\n "request_body": {\n "default": false,\n "description": "Value for request body.",\n "type": "boolean"\n },\n "response_body": {\n "default": false,\n "description": "Value for response body.",\n "type": "boolean"\n }\n },\n "type": "object"\n },\n "secret_references": {\n "description": "Value for secret references.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "start_origin": {\n "description": "Value for start origin.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "start_origin",\n "action_count",\n "secret_references"\n ],\n "type": "object"\n },\n "steps": {\n "description": "Value for steps.",\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "description": "Value for action.",\n "minLength": 1,\n "type": "string"\n },\n "after_url": {\n "description": "Value for after url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "artifacts": {\n "additionalProperties": false,\n "description": "Value for artifacts.",\n "properties": {\n "accessibility": {\n "description": "Value for accessibility.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "dom": {\n "description": "Value for dom.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "history": {\n "description": "Value for history.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "current_url": {\n "description": "Value for current url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "length": {\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "navigation_entries": {\n "description": "Value for navigation entries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "type": {\n "description": "Value for type.",\n "enum": [\n "navigate",\n "reload",\n "back_forward",\n "prerender",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "type",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "length",\n "current_url",\n "navigation_entries"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "screenshot": {\n "description": "Value for screenshot.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "description": "Value for data base64.",\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "description": "Value for media type.",\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "description": "Value for storage.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "cookies": {\n "description": "Value for cookies.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "local_storage": {\n "description": "Value for local storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "session_storage": {\n "description": "Value for session storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "cookies",\n "local_storage",\n "session_storage"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "description": "Value for url.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "object"\n },\n "before_url": {\n "description": "Value for before url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "elapsed_ms": {\n "description": "Value for elapsed ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "description": "Structured, caller-actionable error when the operation fails.",\n "type": "null"\n },\n "event_sequence_end": {\n "description": "Value for event sequence end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "event_sequence_start": {\n "description": "Value for event sequence start.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "const": "completed",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_index",\n "step_id",\n "action",\n "elapsed_ms",\n "before_url",\n "after_url",\n "event_sequence_start",\n "event_sequence_end",\n "artifacts",\n "completeness",\n "status",\n "error"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "description": "Value for action.",\n "minLength": 1,\n "type": "string"\n },\n "after_url": {\n "description": "Value for after url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "artifacts": {\n "additionalProperties": false,\n "description": "Value for artifacts.",\n "properties": {\n "accessibility": {\n "description": "Value for accessibility.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "dom": {\n "description": "Value for dom.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "description": "Value for text.",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "history": {\n "description": "Value for history.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "current_url": {\n "description": "Value for current url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "length": {\n "description": "Value for length.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "navigation_entries": {\n "description": "Value for navigation entries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "type": {\n "description": "Value for type.",\n "enum": [\n "navigate",\n "reload",\n "back_forward",\n "prerender",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "type",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "length",\n "current_url",\n "navigation_entries"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "screenshot": {\n "description": "Value for screenshot.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "description": "Value for data base64.",\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "description": "Value for media type.",\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "description": "Value for storage.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "description": "Value for value.",\n "properties": {\n "cookies": {\n "description": "Value for cookies.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "type": "string"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "local_storage": {\n "description": "Value for local storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "session_storage": {\n "description": "Value for session storage.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "type": "string"\n },\n "value_sha256": {\n "description": "Exact SHA-256 digest of value.",\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "description": "Value for value state.",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "cookies",\n "local_storage",\n "session_storage"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "description": "Value for url.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "description": "Value for state.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "description": "Value for observed.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "description": "Value for retained.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "description": "Value for state.",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "object"\n },\n "before_url": {\n "description": "Value for before url.",\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for origin."\n },\n "query_parameter_names": {\n "description": "Value for query parameter names.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "description": "Value for redacted.",\n "type": "boolean"\n },\n "url": {\n "description": "Value for url.",\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "completeness": {\n "description": "Value for completeness.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "description": "Value for equality eligible.",\n "type": "boolean"\n },\n "missing_sections": {\n "description": "Value for missing sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "description": "Current lifecycle or verification status.",\n "type": "string"\n },\n "truncated_sections": {\n "description": "Value for truncated sections.",\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "elapsed_ms": {\n "description": "Value for elapsed ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "description": "Structured, caller-actionable error when the operation fails.",\n "type": "string"\n },\n "event_sequence_end": {\n "description": "Value for event sequence end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "event_sequence_start": {\n "description": "Value for event sequence start.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "failed",\n "cancelled"\n ],\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "step_index": {\n "description": "Value for step index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_index",\n "step_id",\n "action",\n "elapsed_ms",\n "before_url",\n "after_url",\n "event_sequence_start",\n "event_sequence_end",\n "artifacts",\n "completeness",\n "status",\n "error"\n ],\n "type": "object"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "browser",\n "scenario",\n "duration_ms",\n "steps",\n "events",\n "completeness",\n "limitations"\n ],\n "type": "object"\n },\n "normalization": {\n "additionalProperties": false,\n "description": "Exact-literal normalization policy.",\n "properties": {\n "rules": {\n "default": [],\n "description": "Value for rules.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifacts": {\n "description": "Artifact families whose durable string fields use this rule.",\n "items": {\n "enum": [\n "action_state",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "match": {\n "description": "Exact public literal to replace; regular expressions are not used.",\n "minLength": 1,\n "type": "string"\n },\n "replacement": {\n "description": "Recorded replacement text used in normalized comparison.",\n "type": "string"\n },\n "rule_id": {\n "description": "Stable rule identity; rules execute in rule_id order.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "rule_id",\n "artifacts",\n "match",\n "replacement"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "type": "object"\n }\n },\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "compare_web_captures",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "alignment": {\n "additionalProperties": false,\n "properties": {\n "after_only": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "aligned_steps": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "before_only": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "failures": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "duplicate_before_step_id",\n "duplicate_after_step_id",\n "missing_before_step",\n "missing_after_step",\n "action_mismatch",\n "capture_context_mismatch"\n ],\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "step_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "step_id",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "aligned",\n "partial",\n "failed"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "aligned_steps",\n "before_only",\n "after_only",\n "failures"\n ],\n "type": "object"\n },\n "artifact_diffs": {\n "additionalProperties": false,\n "properties": {\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total"\n ],\n "type": "object"\n },\n "comparison_kind": {\n "const": "browser_scenario",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "normalization": {\n "additionalProperties": false,\n "properties": {\n "built_in_rules": {\n "items": {\n "enum": [\n "ignore_step_elapsed_ms",\n "ignore_event_sequence",\n "ignore_event_step_index",\n "compare_screenshot_content_digest",\n "compare_normalized_text_content"\n ],\n "type": "string"\n },\n "maxItems": 5,\n "minItems": 5,\n "type": "array"\n },\n "rules": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifacts": {\n "description": "Artifact families whose durable string fields use this rule.",\n "items": {\n "enum": [\n "action_state",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "match": {\n "description": "Exact public literal to replace; regular expressions are not used.",\n "minLength": 1,\n "type": "string"\n },\n "replacement": {\n "description": "Recorded replacement text used in normalized comparison.",\n "type": "string"\n },\n "rule_id": {\n "description": "Stable rule identity; rules execute in rule_id order.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "rule_id",\n "artifacts",\n "match",\n "replacement"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "built_in_rules",\n "rules",\n "sha256"\n ],\n "type": "object"\n },\n "overall_status": {\n "enum": [\n "changed",\n "unchanged",\n "unknown"\n ],\n "type": "string"\n },\n "steps": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "after_action": {\n "minLength": 1,\n "type": "string"\n },\n "after_step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "artifact_diffs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "after_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "after_state": {\n "enum": [\n "captured",\n "not_requested",\n "missing",\n "truncated"\n ],\n "type": "string"\n },\n "artifact": {\n "enum": [\n "action_state",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events"\n ],\n "type": "string"\n },\n "before_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "before_state": {\n "enum": [\n "captured",\n "not_requested",\n "missing",\n "truncated"\n ],\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "changed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "artifact",\n "status",\n "before_state",\n "after_state",\n "before_sha256",\n "after_sha256",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "before_action": {\n "minLength": 1,\n "type": "string"\n },\n "before_step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "changed",\n "unchanged",\n "unknown"\n ],\n "type": "string"\n },\n "step_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "step_id",\n "before_step_index",\n "after_step_index",\n "before_action",\n "after_action",\n "status",\n "artifact_diffs"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "comparison_kind",\n "overall_status",\n "normalization",\n "alignment",\n "steps",\n "artifact_diffs",\n "limitations"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after_target": {\n "additionalProperties": false,\n "properties": {\n "target_id": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "url"\n ],\n "type": "object"\n },\n "before_target": {\n "additionalProperties": false,\n "properties": {\n "target_id": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "url"\n ],\n "type": "object"\n },\n "dimensions": {\n "additionalProperties": false,\n "properties": {\n "accessibility": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ],\n "default": {\n "changes": [],\n "reason": "Dimension was not recorded by this version 1 capture diff.",\n "status": "unknown",\n "total_changes": 0\n }\n },\n "dom_structure": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "metadata": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "network": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "resources": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "scripts": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ],\n "default": {\n "changes": [],\n "reason": "Dimension was not recorded by this version 1 capture diff.",\n "status": "unknown",\n "total_changes": 0\n }\n },\n "webmcp": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change": {\n "enum": [\n "added",\n "removed",\n "modified"\n ],\n "type": "string"\n },\n "identity": {\n "type": "string"\n }\n },\n "required": [\n "identity",\n "change"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "total_changes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "total_changes",\n "changes",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n },\n "total_changes": {\n "const": 0,\n "type": "number"\n }\n },\n "required": [\n "total_changes",\n "changes",\n "status",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "dom_structure",\n "scripts",\n "resources",\n "network",\n "metadata",\n "webmcp",\n "accessibility",\n "storage"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "overall_status": {\n "enum": [\n "changed",\n "unchanged",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "overall_status",\n "before_target",\n "after_target",\n "dimensions",\n "limitations"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Compare Web Captures"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Capture the current visible viewport of one configured page as an inline, content-addressed PNG artifact. The operation never scrolls, navigates, or evaluates page JavaScript.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "capture_web_screenshot",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n },\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "captured_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "completeness": {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "url",\n "origin"\n ],\n "type": "object"\n },\n "viewport": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "captured_at",\n "viewport",\n "artifact",\n "completeness",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Capture Web Screenshot"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare two self-verifying PNG screenshot artifacts with local pixel metrics. Returns exact changed-pixel ratios and channel deltas without OCR or external services.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "after": {\n "bytes": 70,\n "data_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PzWvWQAAAABJRU5ErkJggg==",\n "media_type": "image/png",\n "sha256": "153cf6c9a526a63053a37b10234c2fd85df38887c2dc0a800d90abfa6631d01c"\n },\n "before": {\n "bytes": 70,\n "data_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PzWvWQAAAABJRU5ErkJggg==",\n "media_type": "image/png",\n "sha256": "153cf6c9a526a63053a37b10234c2fd85df38887c2dc0a800d90abfa6631d01c"\n },\n "channel_threshold": 0\n }\n ],\n "properties": {\n "after": {\n "description": "The later or right-hand observation to compare.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "description": "Value for data base64.",\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "description": "Value for media type.",\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n },\n "before": {\n "description": "The earlier or left-hand observation to compare.",\n "properties": {\n "bytes": {\n "description": "Value for bytes.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "description": "Value for data base64.",\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "description": "Value for media type.",\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n },\n "channel_threshold": {\n "default": 0,\n "description": "Value for channel threshold.",\n "maximum": 255,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "before",\n "after"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "compare_web_screenshots",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n },\n "before": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n },\n "changed_pixels": {\n "const": 0,\n "type": "number"\n },\n "changed_ratio": {\n "const": 0,\n "type": "number"\n },\n "channel_threshold": {\n "maximum": 255,\n "minimum": 0,\n "type": "integer"\n },\n "compared_pixels": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "maximum_channel_delta": {\n "maximum": 255,\n "minimum": 0,\n "type": "integer"\n },\n "mean_absolute_channel_delta": {\n "maximum": 255,\n "minimum": 0,\n "type": "number"\n },\n "status": {\n "const": "identical",\n "type": "string"\n }\n },\n "required": [\n "before",\n "after",\n "channel_threshold",\n "limitations",\n "status",\n "compared_pixels",\n "changed_pixels",\n "changed_ratio",\n "maximum_channel_delta",\n "mean_absolute_channel_delta"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n },\n "before": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n },\n "changed_pixels": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "changed_ratio": {\n "exclusiveMinimum": 0,\n "maximum": 1,\n "type": "number"\n },\n "channel_threshold": {\n "maximum": 255,\n "minimum": 0,\n "type": "integer"\n },\n "compared_pixels": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "maximum_channel_delta": {\n "exclusiveMinimum": 0,\n "maximum": 255,\n "type": "integer"\n },\n "mean_absolute_channel_delta": {\n "exclusiveMinimum": 0,\n "maximum": 255,\n "type": "number"\n },\n "status": {\n "const": "different",\n "type": "string"\n }\n },\n "required": [\n "before",\n "after",\n "channel_threshold",\n "limitations",\n "status",\n "compared_pixels",\n "changed_pixels",\n "changed_ratio",\n "maximum_channel_delta",\n "mean_absolute_channel_delta"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n },\n "before": {\n "additionalProperties": false,\n "properties": {\n "height": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "width": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "width",\n "height"\n ],\n "type": "object"\n },\n "changed_pixels": {\n "type": "null"\n },\n "changed_ratio": {\n "type": "null"\n },\n "channel_threshold": {\n "maximum": 255,\n "minimum": 0,\n "type": "integer"\n },\n "compared_pixels": {\n "const": 0,\n "type": "number"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "maximum_channel_delta": {\n "type": "null"\n },\n "mean_absolute_channel_delta": {\n "type": "null"\n },\n "status": {\n "const": "dimension_mismatch",\n "type": "string"\n }\n },\n "required": [\n "before",\n "after",\n "channel_threshold",\n "limitations",\n "status",\n "compared_pixels",\n "changed_pixels",\n "changed_ratio",\n "maximum_channel_delta",\n "mean_absolute_channel_delta"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Compare Web Screenshots"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Run a controlled browser scenario when passive observation cannot exercise the application. Launch the selected executable or connect to a loopback CDP target, provide a start URL and actions, and select storage or artifacts when needed. Environment, empty storage, and a URL-only final capture are defaults. capture.network selects request_body, response_body, and header_values independently; selected content is returned inline with transaction and event references, byte digests, and explicit unavailable states. Credentials and declared secrets are redacted. Agents can run their own local fixture servers to control application responses. Actions run until completion or cancellation unless timeout_ms is set. Results are returned inline.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": true,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": true,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "actions": [\n {\n "action": "wait_for_timeout",\n "duration_ms": 250,\n "step_id": "settle"\n }\n ],\n "browser": {\n "executable_path": "/opt/chromium/chrome",\n "mode": "launch"\n },\n "start_url": {\n "query": [],\n "url": "https://app.example.test/"\n }\n }\n ],\n "properties": {\n "actions": {\n "description": "Required sequence of explicit browser actions.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "goto",\n "description": "Value for action.",\n "type": "string"\n },\n "destination": {\n "additionalProperties": false,\n "description": "Value for destination.",\n "properties": {\n "query": {\n "default": [],\n "description": "Non-empty feature or text query to investigate.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "url": {\n "description": "Value for url.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "url"\n ],\n "type": "object"\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "wait_until": {\n "description": "Value for wait until.",\n "enum": [\n "commit",\n "domcontentloaded",\n "load"\n ],\n "type": "string"\n }\n },\n "required": [\n "step_id",\n "action",\n "destination",\n "wait_until"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "click",\n "description": "Value for action.",\n "type": "string"\n },\n "button": {\n "default": "left",\n "description": "Value for button.",\n "enum": [\n "left",\n "middle",\n "right"\n ],\n "type": "string"\n },\n "click_count": {\n "default": 1,\n "description": "Value for click count.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "locator": {\n "description": "Value for locator.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "test_id",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "role",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "button",\n "checkbox",\n "combobox",\n "dialog",\n "link",\n "listbox",\n "menuitem",\n "option",\n "radio",\n "slider",\n "spinbutton",\n "switch",\n "tab",\n "textbox"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "role",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "css",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "selector"\n ],\n "type": "object"\n }\n ]\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "action",\n "locator"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "fill",\n "description": "Value for action.",\n "type": "string"\n },\n "locator": {\n "description": "Value for locator.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "test_id",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "role",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "button",\n "checkbox",\n "combobox",\n "dialog",\n "link",\n "listbox",\n "menuitem",\n "option",\n "radio",\n "slider",\n "spinbutton",\n "switch",\n "tab",\n "textbox"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "role",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "css",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "selector"\n ],\n "type": "object"\n }\n ]\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "step_id",\n "action",\n "locator",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "press",\n "description": "Value for action.",\n "type": "string"\n },\n "key": {\n "description": "Value for key.",\n "minLength": 1,\n "type": "string"\n },\n "locator": {\n "description": "Value for locator.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "test_id",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "role",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "button",\n "checkbox",\n "combobox",\n "dialog",\n "link",\n "listbox",\n "menuitem",\n "option",\n "radio",\n "slider",\n "spinbutton",\n "switch",\n "tab",\n "textbox"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "role",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "css",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "selector"\n ],\n "type": "object"\n }\n ]\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "action",\n "locator",\n "key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "select_option",\n "description": "Value for action.",\n "type": "string"\n },\n "locator": {\n "description": "Value for locator.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "test_id",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "role",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "button",\n "checkbox",\n "combobox",\n "dialog",\n "link",\n "listbox",\n "menuitem",\n "option",\n "radio",\n "slider",\n "spinbutton",\n "switch",\n "tab",\n "textbox"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "role",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "css",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "selector"\n ],\n "type": "object"\n }\n ]\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "step_id",\n "action",\n "locator",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "description": "Value for action.",\n "enum": [\n "check",\n "uncheck"\n ],\n "type": "string"\n },\n "locator": {\n "description": "Value for locator.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "test_id",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "role",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "button",\n "checkbox",\n "combobox",\n "dialog",\n "link",\n "listbox",\n "menuitem",\n "option",\n "radio",\n "slider",\n "spinbutton",\n "switch",\n "tab",\n "textbox"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "role",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "css",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "selector"\n ],\n "type": "object"\n }\n ]\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "action",\n "locator"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "wait_for",\n "description": "Value for action.",\n "type": "string"\n },\n "locator": {\n "description": "Value for locator.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "test_id",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "role",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "button",\n "checkbox",\n "combobox",\n "dialog",\n "link",\n "listbox",\n "menuitem",\n "option",\n "radio",\n "slider",\n "spinbutton",\n "switch",\n "tab",\n "textbox"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "role",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "css",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "selector"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "description": "Value for state.",\n "enum": [\n "attached",\n "detached",\n "visible",\n "hidden"\n ],\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "timeout_ms": {\n "description": "Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "action",\n "locator",\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "const": "wait_for_timeout",\n "description": "Value for action.",\n "type": "string"\n },\n "duration_ms": {\n "description": "Wait this many milliseconds; the wait ends early on cancellation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_id": {\n "description": "Value for step id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n }\n },\n "required": [\n "step_id",\n "action",\n "duration_ms"\n ],\n "type": "object"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n },\n "browser": {\n "description": "Required launch/connect selection. Launch uses the selected executable and an owned temporary profile; connect uses one loopback CDP endpoint and target.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "executable_path": {\n "description": "Local filesystem path for executable.",\n "minLength": 1,\n "type": "string"\n },\n "headless": {\n "default": true,\n "description": "Run without a visible browser window; defaults to true and can be disabled when the host supports a display.",\n "type": "boolean"\n },\n "mode": {\n "const": "launch",\n "description": "Operation mode that selects the requested behavior.",\n "type": "string"\n }\n },\n "required": [\n "mode",\n "executable_path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "mode": {\n "const": "connect",\n "description": "Operation mode that selects the requested behavior.",\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mode",\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n }\n ]\n },\n "capture": {\n "additionalProperties": false,\n "default": {\n "after_each_step": [],\n "at_end": [\n "url"\n ],\n "events": [],\n "network": {\n "header_values": false,\n "request_body": false,\n "response_body": false\n }\n },\n "description": "Optional retained artifacts and event families. Defaults to only a final sanitized URL; request screenshots, DOM, accessibility, history, storage, and event capture explicitly.",\n "properties": {\n "after_each_step": {\n "default": [],\n "description": "Value for after each step.",\n "items": {\n "enum": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "at_end": {\n "default": [\n "url"\n ],\n "description": "Value for at end.",\n "items": {\n "enum": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "events": {\n "default": [],\n "description": "Value for events.",\n "items": {\n "enum": [\n "console",\n "page-errors",\n "network",\n "websockets",\n "frames",\n "workers",\n "popups",\n "downloads"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "network": {\n "additionalProperties": false,\n "default": {\n "header_values": false,\n "request_body": false,\n "response_body": false\n },\n "description": "Value for network.",\n "properties": {\n "header_values": {\n "default": false,\n "description": "Value for header values.",\n "type": "boolean"\n },\n "request_body": {\n "default": false,\n "description": "Value for request body.",\n "type": "boolean"\n },\n "response_body": {\n "default": false,\n "description": "Value for response body.",\n "type": "boolean"\n }\n },\n "type": "object"\n }\n },\n "type": "object"\n },\n "environment": {\n "additionalProperties": false,\n "default": {\n "color_scheme": "light",\n "locale": "en-US",\n "reduced_motion": "reduce",\n "service_workers": "block",\n "timezone": "UTC",\n "viewport": {\n "device_scale_factor": 1,\n "height": 720,\n "width": 1280\n }\n },\n "description": "Optional deterministic browser settings. Defaults to 1280x720, en-US, UTC, light mode, reduced motion, and blocked service workers.",\n "properties": {\n "color_scheme": {\n "default": "light",\n "description": "Value for color scheme.",\n "enum": [\n "light",\n "dark",\n "no-preference"\n ],\n "type": "string"\n },\n "locale": {\n "default": "en-US",\n "description": "Value for locale.",\n "minLength": 2,\n "pattern": "^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$",\n "type": "string"\n },\n "reduced_motion": {\n "default": "reduce",\n "description": "Value for reduced motion.",\n "enum": [\n "reduce",\n "no-preference"\n ],\n "type": "string"\n },\n "service_workers": {\n "default": "block",\n "description": "Value for service workers.",\n "enum": [\n "allow",\n "block"\n ],\n "type": "string"\n },\n "timezone": {\n "default": "UTC",\n "description": "Value for timezone.",\n "minLength": 1,\n "pattern": "^[A-Za-z0-9_+-]+(?:\\\\/[A-Za-z0-9_+-]+)*$",\n "type": "string"\n },\n "viewport": {\n "additionalProperties": false,\n "default": {\n "device_scale_factor": 1,\n "height": 720,\n "width": 1280\n },\n "description": "Value for viewport.",\n "properties": {\n "device_scale_factor": {\n "default": 1,\n "description": "Value for device scale factor.",\n "exclusiveMinimum": 0,\n "type": "number"\n },\n "height": {\n "default": 720,\n "description": "Value for height.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "width": {\n "default": 1280,\n "description": "Value for width.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "type": "object"\n }\n },\n "type": "object"\n },\n "secrets": {\n "default": [],\n "description": "Optional environment-variable references for values used in actions, URLs, storage, or replay, and for redacting matching observed text. Secret values are never supplied inline.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "environment_variable": {\n "description": "Value for environment variable.",\n "minLength": 1,\n "type": "string"\n },\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n }\n },\n "required": [\n "secret_id",\n "environment_variable"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "start_url": {\n "additionalProperties": false,\n "description": "Required initial HTTP(S) URL. Query values must be declared separately as public literals or secret references.",\n "properties": {\n "query": {\n "default": [],\n "description": "Non-empty feature or text query to investigate.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "url": {\n "description": "Value for url.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "url"\n ],\n "type": "object"\n },\n "storage": {\n "additionalProperties": false,\n "description": "Optional initial cookies, local storage, or session storage. Defaults to empty; every supplied value and origin must be declared and within the allowed origins.",\n "properties": {\n "cookies": {\n "description": "Value for cookies.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "destination": {\n "additionalProperties": false,\n "description": "Value for destination.",\n "properties": {\n "query": {\n "default": [],\n "description": "Non-empty feature or text query to investigate.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "url": {\n "description": "Value for url.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "url"\n ],\n "type": "object"\n },\n "http_only": {\n "description": "Value for http only.",\n "type": "boolean"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "same_site": {\n "description": "Value for same site.",\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "description": "Value for secure.",\n "type": "boolean"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "name",\n "value",\n "destination",\n "http_only",\n "secure",\n "same_site"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "local_storage": {\n "description": "Value for local storage.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "entries": {\n "description": "Value for entries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "origin": {\n "description": "Value for origin.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "origin",\n "entries"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "session_storage": {\n "description": "Value for session storage.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "entries": {\n "description": "Value for entries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "source": {\n "const": "literal",\n "description": "Value for source.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "secret_id": {\n "description": "Value for secret id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "source": {\n "const": "secret",\n "description": "Value for source.",\n "type": "string"\n }\n },\n "required": [\n "source",\n "secret_id"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "name",\n "value"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "origin": {\n "description": "Value for origin.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "origin",\n "entries"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "type": "object"\n }\n },\n "required": [\n "browser",\n "start_url",\n "actions"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "capture_browser_scenario",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "cleanup": {\n "enum": [\n "terminated-owned-process",\n "disconnected-external"\n ],\n "type": "string"\n },\n "mode": {\n "enum": [\n "launch",\n "connect"\n ],\n "type": "string"\n },\n "process_ownership": {\n "enum": [\n "provider-owned",\n "external"\n ],\n "type": "string"\n },\n "product": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mode",\n "process_ownership",\n "cleanup",\n "product",\n "version"\n ],\n "type": "object"\n },\n "completeness": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "duration_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "events": {\n "additionalProperties": false,\n "properties": {\n "dropped": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "items": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "console",\n "type": "string"\n },\n "level": {\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text": {\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "level",\n "text",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "page-error",\n "type": "string"\n },\n "message": {\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "stack": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "message",\n "stack"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "type": "null"\n },\n "header_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request",\n "type": "string"\n },\n "method": {\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_type": {\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "type": "null"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "type": "null"\n },\n "header_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "response",\n "type": "string"\n },\n "method": {\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_type": {\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "maximum": 599,\n "minimum": 100,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "minLength": 1,\n "type": "string"\n },\n "header_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-failed",\n "type": "string"\n },\n "method": {\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_type": {\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "type": "null"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "type": "null"\n },\n "header_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-finished",\n "type": "string"\n },\n "method": {\n "minLength": 1,\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_type": {\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "type": "null"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "failure": {\n "type": "null"\n },\n "header_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "const": "request-unfinished",\n "type": "string"\n },\n "method": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "capture-ended",\n "cancelled"\n ],\n "type": "string"\n },\n "redirected_from_transaction_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_type": {\n "minLength": 1,\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "type": "null"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "method",\n "url",\n "resource_type",\n "header_names",\n "kind",\n "status",\n "failure",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "body": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_exposed",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "request-failed",\n "response-unfinished",\n "body-read-failed",\n "body-read-timeout",\n "cancelled",\n "capture-ended"\n ],\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "content": {\n "type": "string"\n },\n "encoding": {\n "const": "base64",\n "type": "string"\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "redacted": {\n "type": "boolean"\n },\n "representation": {\n "enum": [\n "browser-exposed-request-bytes",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "captured",\n "type": "string"\n }\n },\n "required": [\n "state",\n "representation",\n "encoding",\n "content",\n "bytes",\n "sha256",\n "media_type",\n "redacted"\n ],\n "type": "object"\n }\n ]\n },\n "headers": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "message"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "redacted": {\n "type": "boolean"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "value",\n "redacted"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "const": "captured",\n "type": "string"\n }\n },\n "required": [\n "state",\n "items"\n ],\n "type": "object"\n }\n ]\n },\n "kind": {\n "const": "network-content",\n "type": "string"\n },\n "phase": {\n "enum": [\n "request",\n "response"\n ],\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source_event_sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "transaction_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "transaction_id",\n "source_event_sequence",\n "phase",\n "headers",\n "body"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "websocket-opened",\n "websocket-closed"\n ],\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "websocket-frame-sent",\n "websocket-frame-received"\n ],\n "type": "string"\n },\n "payload_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_text": {\n "type": "string"\n },\n "payload_type": {\n "const": "text",\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "payload_bytes",\n "truncated",\n "payload_type",\n "payload_text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "websocket-frame-sent",\n "websocket-frame-received"\n ],\n "type": "string"\n },\n "payload_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "payload_text": {\n "type": "null"\n },\n "payload_type": {\n "const": "binary",\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "payload_bytes",\n "truncated",\n "payload_type",\n "payload_text"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "frame-attached",\n "frame-detached",\n "frame-navigated",\n "worker-created",\n "worker-closed",\n "popup-opened",\n "popup-closed"\n ],\n "type": "string"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "url": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "url",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "download-cancelled",\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "suggested_filename": {\n "type": "string"\n },\n "url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "sequence",\n "step_index",\n "kind",\n "suggested_filename",\n "url"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "retained",\n "dropped",\n "items"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "scenario": {\n "additionalProperties": false,\n "properties": {\n "action_count": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "network_content": {\n "additionalProperties": false,\n "properties": {\n "header_values": {\n "default": false,\n "type": "boolean"\n },\n "request_body": {\n "default": false,\n "type": "boolean"\n },\n "response_body": {\n "default": false,\n "type": "boolean"\n }\n },\n "required": [\n "request_body",\n "response_body",\n "header_values"\n ],\n "type": "object"\n },\n "secret_references": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "start_origin": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "start_origin",\n "action_count",\n "secret_references"\n ],\n "type": "object"\n },\n "steps": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n },\n "after_url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "artifacts": {\n "additionalProperties": false,\n "properties": {\n "accessibility": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "dom": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "history": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "current_url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "navigation_entries": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "type": {\n "enum": [\n "navigate",\n "reload",\n "back_forward",\n "prerender",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "type",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "length",\n "current_url",\n "navigation_entries"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "screenshot": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "cookies": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "type": "string"\n },\n "http_only": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "path": {\n "type": "string"\n },\n "same_site": {\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "type": "boolean"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "type": "string"\n },\n "http_only": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "path": {\n "type": "string"\n },\n "same_site": {\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "type": "boolean"\n },\n "value_sha256": {\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "local_storage": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "session_storage": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "cookies",\n "local_storage",\n "session_storage"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "object"\n },\n "before_url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "completeness": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "type": "null"\n },\n "event_sequence_end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "event_sequence_start": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "const": "completed",\n "type": "string"\n },\n "step_id": {\n "minLength": 1,\n "type": "string"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_index",\n "step_id",\n "action",\n "elapsed_ms",\n "before_url",\n "after_url",\n "event_sequence_start",\n "event_sequence_end",\n "artifacts",\n "completeness",\n "status",\n "error"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "action": {\n "minLength": 1,\n "type": "string"\n },\n "after_url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "artifacts": {\n "additionalProperties": false,\n "properties": {\n "accessibility": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "dom": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "text"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "history": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "current_url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "navigation_entries": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "type": {\n "enum": [\n "navigate",\n "reload",\n "back_forward",\n "prerender",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "type",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "length",\n "current_url",\n "navigation_entries"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "screenshot": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "data_base64": {\n "type": "string"\n },\n "media_type": {\n "const": "image/png",\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "data_base64"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "storage": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "cookies": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "type": "string"\n },\n "http_only": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "path": {\n "type": "string"\n },\n "same_site": {\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "type": "boolean"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "domain": {\n "type": "string"\n },\n "http_only": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "path": {\n "type": "string"\n },\n "same_site": {\n "enum": [\n "Strict",\n "Lax",\n "None"\n ],\n "type": "string"\n },\n "secure": {\n "type": "boolean"\n },\n "value_sha256": {\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "type": "string"\n }\n },\n "required": [\n "name",\n "domain",\n "path",\n "secure",\n "http_only",\n "same_site",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "local_storage": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "session_storage": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "value_state": {\n "const": "hashed",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "value_sha256": {\n "type": "null"\n },\n "value_state": {\n "const": "redacted-secret",\n "type": "string"\n }\n },\n "required": [\n "name",\n "value_state",\n "value_sha256"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "cookies",\n "local_storage",\n "session_storage"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "captured",\n "type": "string"\n },\n "value": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "not_requested",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "missing",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "truncated",\n "type": "string"\n }\n },\n "required": [\n "state",\n "observed",\n "retained",\n "reason"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage"\n ],\n "type": "object"\n },\n "before_url": {\n "additionalProperties": false,\n "properties": {\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "query_parameter_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "redacted": {\n "type": "boolean"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "url",\n "origin",\n "query_parameter_names",\n "redacted"\n ],\n "type": "object"\n },\n "completeness": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n },\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "type": "string"\n },\n "event_sequence_end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "event_sequence_start": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "enum": [\n "failed",\n "cancelled"\n ],\n "type": "string"\n },\n "step_id": {\n "minLength": 1,\n "type": "string"\n },\n "step_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_index",\n "step_id",\n "action",\n "elapsed_ms",\n "before_url",\n "after_url",\n "event_sequence_start",\n "event_sequence_end",\n "artifacts",\n "completeness",\n "status",\n "error"\n ],\n "type": "object"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "browser",\n "scenario",\n "duration_ms",\n "steps",\n "events",\n "completeness",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Capture Browser Scenario"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "List every Electron file:// page target from a selected loopback CDP endpoint. Local file paths are canonicalized after symlink resolution. Selecting an endpoint exposes every eligible target and its local path metadata.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9223"\n }\n ],\n "properties": {\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint"\n ],\n "type": "object"\n },\n "kind": "electron-provider",\n "name": "list_electron_targets",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "excluded": {\n "additionalProperties": false,\n "properties": {\n "non_page": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unsupported_url": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unsupported_url",\n "non_page"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "targets": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "file_path": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n },\n "title": {\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "title",\n "file_path",\n "attached"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "browser",\n "targets",\n "excluded",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Electron Targets"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Passively inspect one Electron file page by supplying its loopback CDP endpoint and target ID. The provider rechecks that the target currently exists at that endpoint and is a local file page. Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs. The selected endpoint exposes every local file page and its metadata to this tool.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "include_script_sources": {\n "const": false,\n "default": false,\n "description": "Whether to include script sources in the result.",\n "type": "boolean"\n },\n "observation_ms": {\n "default": 100,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "include_script_sources": {\n "const": true,\n "description": "Whether to include script sources in the result.",\n "type": "boolean"\n },\n "observation_ms": {\n "default": 100,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id",\n "include_script_sources"\n ],\n "type": "object"\n }\n ],\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9223",\n "target_id": "TARGET_ID_FROM_LIST_ELECTRON_TARGETS"\n }\n ],\n "type": "object"\n },\n "kind": "electron-provider",\n "name": "inspect_electron_page",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "capture_window": {\n "additionalProperties": false,\n "properties": {\n "ended_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "observation_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "started_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n }\n },\n "required": [\n "started_at",\n "ended_at",\n "observation_ms"\n ],\n "type": "object"\n },\n "completeness": {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n "dom": {\n "additionalProperties": false,\n "properties": {\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attribute_names": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_name": {\n "type": "string"\n },\n "node_type": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "node_value_length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parent_index": {\n "maximum": 9007199254740991,\n "minimum": -1,\n "type": "integer"\n }\n },\n "required": [\n "index",\n "parent_index",\n "node_type",\n "node_name",\n "node_value_length",\n "attribute_names"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_nodes",\n "nodes"\n ],\n "type": "object"\n },\n "frames": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "file_path": {\n "type": "string"\n },\n "frame_id": {\n "type": "string"\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "frame_id",\n "parent_frame_id",\n "file_path"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "resources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "content_size": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "file_path": {\n "type": "string"\n },\n "mime_type": {\n "type": "string"\n },\n "resource_key": {\n "pattern": "^electron_resource_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "resource_key",\n "file_path",\n "type",\n "mime_type",\n "content_size"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "scripts": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "cdp_hash": {\n "type": "string"\n },\n "file_path": {\n "type": "string"\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "is_module": {\n "type": "boolean"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "script_key": {\n "pattern": "^electron_script_[a-f0-9]{64}$",\n "type": "string"\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "included": {\n "const": false,\n "type": "boolean"\n },\n "reason": {\n "type": "string"\n }\n },\n "required": [\n "included",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "charset": {\n "const": "utf-8",\n "type": "string"\n },\n "media_type": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "text": {\n "type": "string"\n }\n },\n "required": [\n "sha256",\n "bytes",\n "media_type",\n "charset",\n "text"\n ],\n "type": "object"\n },\n "included": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "included",\n "artifact"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "script_key",\n "frame_id",\n "file_path",\n "cdp_hash",\n "length",\n "is_module",\n "language",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "items"\n ],\n "type": "object"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "file_path": {\n "type": "string"\n },\n "target_id": {\n "type": "string"\n },\n "title": {\n "type": "string"\n },\n "type": {\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "title",\n "file_path",\n "attached"\n ],\n "type": "object"\n },\n "workers": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "file_path": {\n "type": "string"\n },\n "opener_target_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "parent_frame_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_id": {\n "minLength": 1,\n "type": "string"\n },\n "type": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "type",\n "file_path",\n "attached",\n "opener_target_id",\n "parent_frame_id"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "browser",\n "target",\n "capture_window",\n "completeness",\n "frames",\n "dom",\n "scripts",\n "resources",\n "workers",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Electron Page"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Reconstruct one local ASAR or extracted JavaScript application as an inline application graph without executing it. Returns all recovered graph nodes, edges, semantic relations, limitations, and coverage. Integrity mismatches, such as unpacked native modules signed after packaging, fail by default; record-and-continue analyzes the observed bytes and reports each mismatch as an untrusted integrity contradiction.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "format": "auto",\n "input_path": "/Applications/Example.app/Contents/Resources/app.asar"\n }\n ],\n "properties": {\n "format": {\n "default": "auto",\n "description": "Declared input artifact format.",\n "enum": [\n "auto",\n "asar",\n "directory"\n ],\n "type": "string"\n },\n "input_path": {\n "description": "Local filesystem path for input.",\n "minLength": 1,\n "type": "string"\n },\n "integrity_policy": {\n "default": "fail",\n "description": "Value for integrity policy.",\n "enum": [\n "fail",\n "record-and-continue"\n ],\n "type": "string"\n }\n },\n "required": [\n "input_path"\n ],\n "type": "object"\n },\n "kind": "electron-provider",\n "name": "analyze_javascript_application",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "format": {\n "enum": [\n "asar",\n "directory"\n ],\n "type": "string"\n },\n "graph": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "edge_id": {\n "pattern": "^jag_edge_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "contains",\n "loads",\n "imports",\n "maps_to",\n "exposes",\n "sends",\n "invokes",\n "handles",\n "calls",\n "persists_to",\n "observed_as",\n "changed_from"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "properties",\n "evidence",\n "edge_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "identity": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "global-exact",\n "type": "string"\n },\n "strategy": {\n "const": "content-digest",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "original_source": {\n "type": "string"\n },\n "source_map_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "stability": {\n "const": "source-map-exact",\n "type": "string"\n },\n "strategy": {\n "const": "source-map-original",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "source_map_sha256",\n "original_source",\n "source_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "canonical-path",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "minLength": 1,\n "type": "string"\n },\n "basis": {\n "items": {\n "enum": [\n "syntax-tree",\n "control-flow",\n "imports",\n "exports",\n "literals",\n "api-usage"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "cross-version-inference",\n "type": "string"\n },\n "strategy": {\n "const": "structural-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "algorithm",\n "fingerprint_sha256",\n "basis"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "key": {\n "type": "string"\n },\n "namespace": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "artifact-local-key",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "namespace",\n "key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_key": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "capture-only",\n "type": "string"\n },\n "strategy": {\n "const": "runtime-instance",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "capture_sha256",\n "runtime_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observation_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "observation-only",\n "type": "string"\n },\n "strategy": {\n "const": "observation-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "observation_sha256",\n "scope"\n ],\n "type": "object"\n }\n ]\n },\n "kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "observations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "observation-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "observation_id": {\n "pattern": "^jag_observation_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "label",\n "properties",\n "evidence",\n "observation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "kind",\n "identity",\n "observations"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "root_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "schema": {\n "const": "JavaScriptApplicationGraph",\n "type": "string"\n }\n },\n "required": [\n "schema",\n "root_node_ids",\n "nodes",\n "edges",\n "coverage",\n "limitations",\n "graph_id"\n ],\n "type": "object"\n },\n "input_path": {\n "minLength": 1,\n "type": "string"\n },\n "integrity_contradictions": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "contradiction_id": {\n "pattern": "^ic_[a-f0-9]{64}$",\n "type": "string"\n },\n "declared_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "entry_kind": {\n "enum": [\n "file",\n "slice"\n ],\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "logical_path": {\n "minLength": 1,\n "type": "string"\n },\n "observed_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "occurrence_id": {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n "parent_artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "provenance": {\n "const": "container-integrity-metadata-versus-observed-bytes",\n "type": "string"\n },\n "trust": {\n "const": "observed-untrusted",\n "type": "string"\n },\n "unpacked": {\n "type": "boolean"\n }\n },\n "required": [\n "contradiction_id",\n "occurrence_id",\n "parent_artifact_id",\n "logical_path",\n "declared_sha256",\n "observed_sha256",\n "entry_kind",\n "unpacked",\n "trust",\n "provenance",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "inventory_graph_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "inventory_manifest_id": {\n "pattern": "^agm_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "semantic_graph": {\n "additionalProperties": false,\n "properties": {\n "application_graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "families": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "family": {\n "enum": [\n "boundary",\n "call-flow",\n "child-process",\n "closure",\n "configuration",\n "data-flow",\n "event",\n "object-flow",\n "promise-ownership",\n "request",\n "resource-lifecycle",\n "timer"\n ],\n "type": "string"\n },\n "omitted_relations": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "retained_relations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unsupported"\n ],\n "type": "string"\n },\n "unknown_ids": {\n "items": {\n "pattern": "^jsrg_unknown_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "family",\n "status",\n "retained_relations",\n "omitted_relations",\n "unknown_ids"\n ],\n "type": "object"\n },\n "maxItems": 12,\n "minItems": 12,\n "type": "array"\n },\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_nodes": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "omitted_relations": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_nodes",\n "omitted_relations",\n "limits",\n "families"\n ],\n "type": "object"\n },\n "fingerprints": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "const": "rea.javascript-semantic-function",\n "type": "string"\n },\n "components": {\n "additionalProperties": false,\n "properties": {\n "control_flow_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "effects": {\n "items": {\n "enum": [\n "async",\n "child-process",\n "event",\n "network",\n "promise",\n "resource",\n "timer"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "literal_set_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "normalized_ast_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameter_arity": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "relation_shape_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "parameter_arity",\n "normalized_ast_sha256",\n "control_flow_sha256",\n "relation_shape_sha256",\n "literal_set_sha256",\n "effects"\n ],\n "type": "object"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "fingerprint_id": {\n "pattern": "^jsrg_fingerprint_[a-f0-9]{64}$",\n "type": "string"\n },\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "function_node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "function_node_id",\n "algorithm",\n "status",\n "components",\n "limitations",\n "evidence",\n "fingerprint_id",\n "fingerprint_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "graph_id": {\n "pattern": "^jsrg_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "application_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "function_node_id": {\n "anyOf": [\n {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "identity": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "role_key": {\n "minLength": 1,\n "type": "string"\n },\n "source_range": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "artifact_sha256",\n "module_path",\n "source_range",\n "role_key"\n ],\n "type": "object"\n },\n "kind": {\n "enum": [\n "module",\n "function",\n "parameter",\n "binding",\n "expression",\n "literal",\n "property-slot",\n "call-site",\n "return-site",\n "closure-capture",\n "promise",\n "task",\n "event",\n "listener",\n "timer",\n "child-process",\n "stdio",\n "signal",\n "config-source",\n "request",\n "response",\n "boundary",\n "resource",\n "unknown-frontier"\n ],\n "type": "string"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "kind",\n "identity",\n "function_node_id",\n "application_node_ids",\n "label",\n "properties",\n "evidence",\n "node_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "relations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "acquires",\n "aggregates",\n "aliases",\n "argument-to-parameter",\n "awaits",\n "calls",\n "cancels-timer",\n "captures",\n "chains",\n "coerces",\n "connects-stdio",\n "consumed-by",\n "constructs-request",\n "creates-promise",\n "defaults",\n "defines",\n "destructures",\n "detaches-task",\n "dispatches-candidate",\n "forwards-signal",\n "listens-error",\n "listens-exit",\n "owns",\n "overrides",\n "parses",\n "reads",\n "reads-argv",\n "reads-config",\n "reads-environment",\n "reads-property",\n "registers-listener",\n "releases",\n "removes-listener",\n "returns-task",\n "returns-to-call",\n "schedules-timer",\n "spawns",\n "spreads",\n "supplies-argv",\n "supplies-env",\n "supplies-request-field",\n "validates",\n "writes",\n "writes-property"\n ],\n "type": "string"\n },\n "relation_id": {\n "pattern": "^jsrg_relation_[a-f0-9]{64}$",\n "type": "string"\n },\n "resolution": {\n "enum": [\n "resolved",\n "candidate"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "resolution",\n "properties",\n "evidence",\n "relation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "root_node_ids": {\n "items": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "schema": {\n "const": "JavaScriptSemanticRelationGraph",\n "type": "string"\n },\n "unknowns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "candidate_node_ids": {\n "items": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "family": {\n "enum": [\n "boundary",\n "call-flow",\n "child-process",\n "closure",\n "configuration",\n "data-flow",\n "event",\n "object-flow",\n "promise-ownership",\n "request",\n "resource-lifecycle",\n "timer"\n ],\n "type": "string"\n },\n "node_id": {\n "anyOf": [\n {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "ambiguous-target",\n "bound-reached",\n "dynamic-call",\n "dynamic-property",\n "dynamic-scope",\n "eval-or-generated-code",\n "incomplete-module",\n "missing-source",\n "unsupported-syntax"\n ],\n "type": "string"\n },\n "relation_kinds": {\n "items": {\n "enum": [\n "acquires",\n "aggregates",\n "aliases",\n "argument-to-parameter",\n "awaits",\n "calls",\n "cancels-timer",\n "captures",\n "chains",\n "coerces",\n "connects-stdio",\n "consumed-by",\n "constructs-request",\n "creates-promise",\n "defaults",\n "defines",\n "destructures",\n "detaches-task",\n "dispatches-candidate",\n "forwards-signal",\n "listens-error",\n "listens-exit",\n "owns",\n "overrides",\n "parses",\n "reads",\n "reads-argv",\n "reads-config",\n "reads-environment",\n "reads-property",\n "registers-listener",\n "releases",\n "removes-listener",\n "returns-task",\n "returns-to-call",\n "schedules-timer",\n "spawns",\n "spreads",\n "supplies-argv",\n "supplies-env",\n "supplies-request-field",\n "validates",\n "writes",\n "writes-property"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^jsrg_unknown_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "family",\n "relation_kinds",\n "reason",\n "detail",\n "candidate_node_ids",\n "evidence",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "schema",\n "root_artifact_sha256",\n "application_graph_id",\n "root_node_ids",\n "nodes",\n "relations",\n "fingerprints",\n "unknowns",\n "coverage",\n "limitations",\n "graph_id"\n ],\n "type": "object"\n },\n "statistics": {\n "additionalProperties": false,\n "properties": {\n "findings": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "invalid_utf8_files": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "modules": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nested_asar_containers": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parse_failures": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "parsed_javascript_files": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "relevant_files": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "text_bytes_read": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated_scopes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "visited_ast_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "relevant_files",\n "nested_asar_containers",\n "text_bytes_read",\n "invalid_utf8_files",\n "parsed_javascript_files",\n "visited_ast_nodes",\n "findings",\n "modules",\n "parse_failures",\n "truncated_scopes"\n ],\n "type": "object"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "browser_windows": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "context_bridge_apis": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "explicit_web_preferences": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exposed_api_members": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "ipc": {\n "additionalProperties": false,\n "properties": {\n "ambiguous_renderer_transmissions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "dynamic_channel_operations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "literal_channels": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "main_handlers": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "operations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "paired_renderer_transmissions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "renderer_listeners": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "renderer_transmissions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unpaired_literal_renderer_transmissions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "operations",\n "literal_channels",\n "dynamic_channel_operations",\n "renderer_transmissions",\n "renderer_listeners",\n "main_handlers",\n "paired_renderer_transmissions",\n "ambiguous_renderer_transmissions",\n "unpaired_literal_renderer_transmissions"\n ],\n "type": "object"\n },\n "native_addon_bindings": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "preload_entrypoints": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "resolved_native_addon_bindings": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "resolved_utility_entrypoints": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sender_validation_observations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "utility_processes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "browser_windows",\n "explicit_web_preferences",\n "preload_entrypoints",\n "context_bridge_apis",\n "exposed_api_members",\n "ipc",\n "sender_validation_observations",\n "utility_processes",\n "resolved_utility_entrypoints",\n "native_addon_bindings",\n "resolved_native_addon_bindings"\n ],\n "type": "object"\n }\n },\n "required": [\n "input_path",\n "format",\n "root_artifact_sha256",\n "inventory_manifest_id",\n "inventory_graph_sha256",\n "graph",\n "summary",\n "statistics",\n "integrity_contradictions",\n "limitations",\n "semantic_graph"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Analyze Javascript Application"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Reconcile verified static JavaScript application graphs with existing passive web/Electron CDP, passive V8 Inspector, or provider-owned active Electron Evidence. Active Electron captures contribute an explicitly partial target-only runtime record; they never invent renderer scripts, frames, workers, or execution claims. Exact captured-source digests take priority over caller-declared file/URL mappings; target, frame, script, and worker ambiguity remains explicit, and source-map authority stays separate.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "runtime_observations": [\n {\n "authority": "external-service",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "browser": {\n "js_version": "1",\n "product": "Electron/example",\n "protocol_version": "1.3",\n "revision": "example",\n "user_agent": "Electron example"\n },\n "capture_window": {\n "ended_at": "2026-07-15T00:00:00.100Z",\n "observation_ms": 100,\n "started_at": "2026-07-15T00:00:00.000Z"\n },\n "completeness": {\n "attach_limited_sections": [],\n "conditions": [\n "complete_within_window"\n ],\n "dropped_events": {\n "console_events": 0,\n "network_requests": 0,\n "scripts": 0,\n "timeline_events": 0,\n "total": 0,\n "webmcp_tools": 0,\n "websocket_connections": 0,\n "websocket_frames": 0\n },\n "excluded": [],\n "policy_filtered_sections": [],\n "status": "complete_within_window",\n "truncated_sections": [],\n "unavailable_sections": []\n },\n "dom": {\n "nodes": [],\n "total_nodes": 0\n },\n "frames": [],\n "limitations": [\n "Example passive capture."\n ],\n "resources": [],\n "scripts": {\n "items": [\n {\n "cdp_hash": "example",\n "file_path": "/Applications/Example.app/Contents/Resources/app/renderer.js",\n "frame_id": null,\n "is_module": true,\n "language": "JavaScript",\n "length": 29,\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "source": {\n "artifact": {\n "bytes": 29,\n "charset": "utf-8",\n "media_type": "text/javascript",\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "text": "export const example = true;\\n"\n },\n "included": true\n }\n }\n ],\n "total": 1\n },\n "target": {\n "attached": false,\n "file_path": "/Applications/Example.app/Contents/Resources/app/index.html",\n "target_id": "example-target",\n "title": "Example",\n "type": "page"\n },\n "workers": []\n },\n "operation": "inspect_electron_page",\n "parameters": {\n "cdp_endpoint": "http://127.0.0.1:9223",\n "include_script_sources": true,\n "target_id": "example-target"\n },\n "predicate_type": "rea.electron-page-inspection",\n "provider": {\n "id": "rea-cdp-electron",\n "name": "REA Electron file-page CDP observation provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n ],\n "static_layers": [\n {\n "analysis": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "format": "directory",\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [],\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "limitations": [],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "inventory_graph_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "inventory_manifest_id": "agm_2222222222222222222222222222222222222222222222222222222222222222",\n "limitations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "semantic_graph": {\n "application_graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "coverage": {\n "families": [\n {\n "family": "boundary",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "call-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "child-process",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "closure",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "configuration",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "data-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "event",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "object-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "promise-ownership",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "request",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "resource-lifecycle",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "timer",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n }\n ],\n "limits": [],\n "omitted_nodes": 0,\n "omitted_relations": 0,\n "status": "unknown",\n "truncated": false\n },\n "fingerprints": [],\n "graph_id": "jsrg_08c925d2bf4ef56d770e0e67901d5bdfb610744cf001a50d36c9bb6dcb195508",\n "limitations": [\n "Semantic relations are unavailable in this minimal example."\n ],\n "nodes": [\n {\n "application_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "function_node_id": null,\n "identifier_strategy": {\n "stability": "artifact-version",\n "strategy": "semantic-content-sha256"\n },\n "identity": {\n "artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "module_path": "renderer.js",\n "role_key": "example-module",\n "source_range": null\n },\n "kind": "module",\n "label": "renderer.js",\n "node_id": "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a",\n "properties": {}\n }\n ],\n "relations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "root_node_ids": [\n "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a"\n ],\n "schema": "JavaScriptSemanticRelationGraph",\n "unknowns": []\n },\n "statistics": {\n "findings": 0,\n "invalid_utf8_files": 0,\n "modules": 0,\n "nested_asar_containers": 0,\n "parse_failures": 0,\n "parsed_javascript_files": 1,\n "relevant_files": 1,\n "text_bytes_read": 29,\n "truncated_scopes": 0,\n "visited_ast_nodes": 1\n },\n "summary": {\n "browser_windows": 0,\n "context_bridge_apis": 0,\n "explicit_web_preferences": 0,\n "exposed_api_members": 0,\n "ipc": {\n "ambiguous_renderer_transmissions": 0,\n "dynamic_channel_operations": 0,\n "literal_channels": 0,\n "main_handlers": 0,\n "operations": 0,\n "paired_renderer_transmissions": 0,\n "renderer_listeners": 0,\n "renderer_transmissions": 0,\n "unpaired_literal_renderer_transmissions": 0\n },\n "native_addon_bindings": 0,\n "preload_entrypoints": 0,\n "resolved_native_addon_bindings": 0,\n "resolved_utility_entrypoints": 0,\n "sender_validation_observations": 0,\n "utility_processes": 0\n }\n },\n "operation": "analyze_javascript_application",\n "parameters": {},\n "predicate_type": "rea.javascript-application-analysis",\n "provider": {\n "id": "rea-javascript-application",\n "name": "REA JavaScript application analyzer",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "directory",\n "local_path": "/Applications/Example.app/Contents/Resources/app",\n "name": "app"\n }\n }\n }\n ]\n }\n ],\n "properties": {\n "runtime_observations": {\n "description": "Value for runtime observations.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "static_layers": {\n "description": "Value for static layers.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis": {\n "additionalProperties": false,\n "description": "Value for analysis.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "role": {\n "default": "application",\n "description": "Value for role.",\n "enum": [\n "application",\n "cache",\n "assets"\n ],\n "type": "string"\n },\n "runtime_mappings": {\n "default": [],\n "description": "Value for runtime mappings.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_prefix": {\n "default": "",\n "description": "Value for artifact prefix.",\n "type": "string"\n },\n "kind": {\n "const": "file-root",\n "description": "Value for kind.",\n "type": "string"\n },\n "root": {\n "description": "Value for root.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "root"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_prefix": {\n "default": "",\n "description": "Value for artifact prefix.",\n "type": "string"\n },\n "kind": {\n "const": "url-prefix",\n "description": "Value for kind.",\n "type": "string"\n },\n "prefix": {\n "description": "Value for prefix.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "prefix"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "analysis"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "static_layers",\n "runtime_observations"\n ],\n "type": "object"\n },\n "kind": "electron-provider",\n "name": "reconcile_javascript_runtime",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "omitted_graph_items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "omitted_reconciliation_items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "omitted_runtime_entities": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "omitted_static_load_states": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial",\n "truncated"\n ],\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_runtime_entities",\n "omitted_reconciliation_items",\n "omitted_static_load_states",\n "omitted_graph_items"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "graph": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "edge_id": {\n "pattern": "^jag_edge_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "contains",\n "loads",\n "imports",\n "maps_to",\n "exposes",\n "sends",\n "invokes",\n "handles",\n "calls",\n "persists_to",\n "observed_as",\n "changed_from"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "properties",\n "evidence",\n "edge_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "identity": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "global-exact",\n "type": "string"\n },\n "strategy": {\n "const": "content-digest",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "original_source": {\n "type": "string"\n },\n "source_map_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "stability": {\n "const": "source-map-exact",\n "type": "string"\n },\n "strategy": {\n "const": "source-map-original",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "source_map_sha256",\n "original_source",\n "source_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "canonical-path",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "minLength": 1,\n "type": "string"\n },\n "basis": {\n "items": {\n "enum": [\n "syntax-tree",\n "control-flow",\n "imports",\n "exports",\n "literals",\n "api-usage"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "cross-version-inference",\n "type": "string"\n },\n "strategy": {\n "const": "structural-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "algorithm",\n "fingerprint_sha256",\n "basis"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "key": {\n "type": "string"\n },\n "namespace": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "artifact-local-key",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "namespace",\n "key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_key": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "capture-only",\n "type": "string"\n },\n "strategy": {\n "const": "runtime-instance",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "capture_sha256",\n "runtime_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observation_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "observation-only",\n "type": "string"\n },\n "strategy": {\n "const": "observation-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "observation_sha256",\n "scope"\n ],\n "type": "object"\n }\n ]\n },\n "kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "observations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "observation-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "observation_id": {\n "pattern": "^jag_observation_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "label",\n "properties",\n "evidence",\n "observation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "kind",\n "identity",\n "observations"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "root_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "schema": {\n "const": "JavaScriptApplicationGraph",\n "type": "string"\n }\n },\n "required": [\n "schema",\n "root_node_ids",\n "nodes",\n "edges",\n "coverage",\n "limitations",\n "graph_id"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "reconciliation_id": {\n "pattern": "^jrr_[a-f0-9]{64}$",\n "type": "string"\n },\n "reconciliations": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "content-and-location",\n "content-sha256",\n "module-source-sha256",\n "artifact-path",\n "operator-file-mapping",\n "operator-url-mapping",\n "none"\n ],\n "type": "string"\n },\n "candidate_static_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "candidate_static_nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "static_layer_id": {\n "pattern": "^jrl_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "static_layer_id",\n "static_node_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "entity_kind": {\n "enum": [\n "target",\n "frame",\n "script",\n "worker"\n ],\n "type": "string"\n },\n "reason": {\n "enum": [\n "content-and-location-match",\n "unique-content-match",\n "unique-location-match",\n "ambiguous-static-candidates",\n "captured-content-disagrees-with-static-location",\n "no-static-content-match",\n "no-authorized-location-mapping",\n "runtime-location-unresolved",\n "runtime-frame-unattributed",\n "static-or-runtime-coverage-incomplete",\n "reconciliation-limit-reached"\n ],\n "type": "string"\n },\n "reconciliation_id": {\n "pattern": "^jrr_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_layer_id": {\n "pattern": "^jrl_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "reconciliation_id",\n "entity_kind",\n "runtime_evidence_id",\n "runtime_node_id",\n "basis",\n "confidence",\n "reason",\n "candidate_static_count",\n "candidate_static_nodes",\n "static_layer_id",\n "static_node_id",\n "status"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "content-and-location",\n "content-sha256",\n "module-source-sha256",\n "artifact-path",\n "operator-file-mapping",\n "operator-url-mapping",\n "none"\n ],\n "type": "string"\n },\n "candidate_static_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "candidate_static_nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "static_layer_id": {\n "pattern": "^jrl_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "static_layer_id",\n "static_node_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "entity_kind": {\n "enum": [\n "target",\n "frame",\n "script",\n "worker"\n ],\n "type": "string"\n },\n "reason": {\n "enum": [\n "content-and-location-match",\n "unique-content-match",\n "unique-location-match",\n "ambiguous-static-candidates",\n "captured-content-disagrees-with-static-location",\n "no-static-content-match",\n "no-authorized-location-mapping",\n "runtime-location-unresolved",\n "runtime-frame-unattributed",\n "static-or-runtime-coverage-incomplete",\n "reconciliation-limit-reached"\n ],\n "type": "string"\n },\n "reconciliation_id": {\n "pattern": "^jrr_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_layer_id": {\n "type": "null"\n },\n "static_node_id": {\n "type": "null"\n },\n "status": {\n "enum": [\n "ambiguous",\n "unmatched",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "reconciliation_id",\n "entity_kind",\n "runtime_evidence_id",\n "runtime_node_id",\n "basis",\n "confidence",\n "reason",\n "candidate_static_count",\n "candidate_static_nodes",\n "static_layer_id",\n "static_node_id",\n "status"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "runtime_captures": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "enum": [\n "browser",\n "electron",\n "v8-inspector",\n "electron-active"\n ],\n "type": "string"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts_complete_within_scope": {\n "type": "boolean"\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n },\n "target_location": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "unresolved",\n "type": "string"\n },\n "reason": {\n "const": "unverifiable-file-location",\n "type": "string"\n },\n "reported_url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "reported_url",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "target_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "workers": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "evidence_id",\n "capture_sha256",\n "kind",\n "target_node_id",\n "target_key",\n "target_location",\n "frames",\n "scripts",\n "workers",\n "scripts_complete_within_scope"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "source_map_authority": {\n "additionalProperties": false,\n "properties": {\n "limitation": {\n "minLength": 1,\n "type": "string"\n },\n "runtime_script_declarations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "static_layer_count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "used_for_primary_matching": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "used_for_primary_matching",\n "static_layer_count",\n "runtime_script_declarations",\n "limitation"\n ],\n "type": "object"\n },\n "static_layers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "enum": [\n "asar",\n "directory"\n ],\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "input_path": {\n "minLength": 1,\n "type": "string"\n },\n "layer_id": {\n "pattern": "^jrl_[a-f0-9]{64}$",\n "type": "string"\n },\n "role": {\n "enum": [\n "application",\n "cache",\n "assets"\n ],\n "type": "string"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_mappings": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_prefix": {\n "default": "",\n "type": "string"\n },\n "kind": {\n "const": "file-root",\n "type": "string"\n },\n "root": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "root",\n "artifact_prefix"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_prefix": {\n "default": "",\n "type": "string"\n },\n "kind": {\n "const": "url-prefix",\n "type": "string"\n },\n "prefix": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "prefix",\n "artifact_prefix"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "layer_id",\n "role",\n "evidence_id",\n "graph_id",\n "root_artifact_sha256",\n "input_path",\n "format",\n "runtime_mappings"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "static_load_states": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "enum": [\n "javascript-asset",\n "javascript-chunk",\n "javascript-module"\n ],\n "type": "string"\n },\n "reason": {\n "enum": [\n "runtime-script-correspondence",\n "containing-asset-was-loaded",\n "not-observed-in-bounded-capture",\n "layer-outside-runtime-scope",\n "static-or-runtime-coverage-incomplete"\n ],\n "type": "string"\n },\n "runtime_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "static_layer_id": {\n "pattern": "^jrl_[a-f0-9]{64}$",\n "type": "string"\n },\n "static_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "loaded",\n "resident-in-loaded-asset",\n "not-observed-in-capture",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "static_layer_id",\n "static_node_id",\n "kind",\n "status",\n "runtime_node_ids",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "ambiguous": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "matched": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "runtime_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "runtime_scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "runtime_targets": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "runtime_workers": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "static_loaded": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "static_not_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "static_resident": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "static_unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unmatched": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "runtime_targets",\n "runtime_frames",\n "runtime_scripts",\n "runtime_workers",\n "matched",\n "ambiguous",\n "unmatched",\n "unknown",\n "static_loaded",\n "static_resident",\n "static_not_observed",\n "static_unknown"\n ],\n "type": "object"\n }\n },\n "required": [\n "reconciliation_id",\n "static_layers",\n "runtime_captures",\n "graph",\n "summary",\n "reconciliations",\n "static_load_states",\n "source_map_authority",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Reconcile Javascript Runtime"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Use this for a provider-owned Electron run when passive CDP or Inspector observation cannot exercise application behavior. REA owns startup and teardown, accepts caller-defined click/wait actions plus window-targeted renderer reload/crash and synthetic open-url/second-instance delivery, and returns correlated window/WebContents/process/preload/session/navigation/shell/IPC evidence without retaining payload values. Results identify observed and unavailable event families, coverage status, action targets, and truncation. External shell, navigation, permission, download, popup, updater, and OS-integration effects are blocked and recorded. Use passive Electron tools for observation-only work.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": true,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": true,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "actions": [\n {\n "kind": "click",\n "selector": "#run",\n "step_id": "exercise-ipc"\n }\n ],\n "application_path": "/Applications/Example.app/Contents/Resources/main.js",\n "args": [],\n "executable_path": "/Applications/Electron.app/Contents/MacOS/Electron"\n }\n ],\n "properties": {\n "actions": {\n "default": [],\n "description": "Value for actions.",\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "click",\n "description": "Value for kind.",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "window_index": {\n "default": 0,\n "description": "Value for window index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "kind",\n "selector"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "duration_ms": {\n "description": "Value for duration ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "wait",\n "description": "Value for kind.",\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "window_index": {\n "description": "Value for window index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "kind",\n "duration_ms"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "renderer-reload",\n "description": "Value for kind.",\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "window_index": {\n "default": 0,\n "description": "Value for window index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "renderer-crash",\n "description": "Value for kind.",\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "window_index": {\n "default": 0,\n "description": "Value for window index.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "step_id",\n "kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "delivery": {\n "description": "Value for delivery.",\n "enum": [\n "open-url",\n "second-instance"\n ],\n "type": "string"\n },\n "kind": {\n "const": "deep-link",\n "description": "Value for kind.",\n "type": "string"\n },\n "step_id": {\n "description": "Value for step id.",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Value for url.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "step_id",\n "kind",\n "delivery",\n "url"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "application_path": {\n "description": "Local filesystem path for application.",\n "minLength": 1,\n "type": "string"\n },\n "application_root": {\n "description": "Local filesystem application root selected for this operation.",\n "minLength": 1,\n "type": "string"\n },\n "args": {\n "default": [],\n "description": "Value for args.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "executable_path": {\n "description": "Local filesystem path for executable.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "executable_path",\n "application_path"\n ],\n "type": "object"\n },\n "kind": "electron-provider",\n "name": "capture_electron_scenario",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "actions": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "type": "null"\n },\n "kind": {\n "enum": [\n "click",\n "wait",\n "renderer-reload",\n "renderer-crash",\n "deep-link"\n ],\n "type": "string"\n },\n "status": {\n "const": "completed",\n "type": "string"\n },\n "step_id": {\n "minLength": 1,\n "type": "string"\n },\n "target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "window_index": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "step_id",\n "kind",\n "window_index",\n "target",\n "elapsed_ms",\n "status",\n "error"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "error": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "click",\n "wait",\n "renderer-reload",\n "renderer-crash",\n "deep-link"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "failed",\n "cancelled"\n ],\n "type": "string"\n },\n "step_id": {\n "minLength": 1,\n "type": "string"\n },\n "target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "window_index": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "step_id",\n "kind",\n "window_index",\n "target",\n "elapsed_ms",\n "status",\n "error"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "application": {\n "additionalProperties": false,\n "properties": {\n "application_path": {\n "minLength": 1,\n "type": "string"\n },\n "cleanup": {\n "const": "terminated-owned-process",\n "type": "string"\n },\n "electron_version": {\n "minLength": 1,\n "type": "string"\n },\n "executable_path": {\n "minLength": 1,\n "type": "string"\n },\n "process_ownership": {\n "const": "provider-owned",\n "type": "string"\n }\n },\n "required": [\n "executable_path",\n "application_path",\n "electron_version",\n "process_ownership",\n "cleanup"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "default": {\n "observed_event_families": [],\n "observed_roles": [],\n "pre_capture_activity": "unavailable",\n "status": "partial_attach",\n "unavailable_event_families": []\n },\n "properties": {\n "observed_event_families": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "observed_roles": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "pre_capture_activity": {\n "const": "unavailable",\n "type": "string"\n },\n "status": {\n "enum": [\n "complete",\n "partial_attach",\n "hook_conflict",\n "target_exited",\n "cleanup_failed"\n ],\n "type": "string"\n },\n "unavailable_event_families": {\n "items": {\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "observed_event_families",\n "unavailable_event_families",\n "observed_roles",\n "pre_capture_activity"\n ],\n "type": "object"\n },\n "ipc": {\n "additionalProperties": false,\n "properties": {\n "events": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "argument_shapes": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "artifact_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "capture_method": {\n "default": "api-wrapper",\n "enum": [\n "api-wrapper",\n "event-emitter",\n "process-hook"\n ],\n "type": "string"\n },\n "channel": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "correlation_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "direction": {\n "anyOf": [\n {\n "enum": [\n "renderer-to-main",\n "main-to-renderer",\n "main-to-utility",\n "utility-to-main"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "error": {\n "type": "boolean"\n },\n "event": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "frame": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "kind": {\n "enum": [\n "main-handler-invocation",\n "main-event-invocation",\n "utility-process-fork",\n "utility-process-message",\n "ipc-main-to-renderer",\n "ipc-utility-to-main",\n "ipc-renderer-send",\n "ipc-renderer-invoke",\n "ipc-renderer-post-message"\n ],\n "type": "string"\n },\n "phase": {\n "anyOf": [\n {\n "enum": [\n "attempted",\n "completed",\n "blocked",\n "failed",\n "observed"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "process_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "receiver": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "result_shape": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sender": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source": {\n "default": "electron-active-hook",\n "type": "string"\n },\n "target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "sequence",\n "correlation_id",\n "kind",\n "channel",\n "frame",\n "argument_shapes",\n "result_shape",\n "process_type",\n "source",\n "capture_method",\n "artifact_path",\n "artifact_sha256",\n "error"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "events",\n "observed"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "processes": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "service_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "type": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "pid",\n "type",\n "name",\n "service_name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "items"\n ],\n "type": "object"\n },\n "timeline": {\n "additionalProperties": false,\n "default": {\n "events": [],\n "observed": 0\n },\n "properties": {\n "events": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "argument_shapes": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "artifact_path": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "capture_method": {\n "enum": [\n "api-wrapper",\n "event-emitter",\n "process-hook"\n ],\n "type": "string"\n },\n "channel": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "correlation_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "direction": {\n "anyOf": [\n {\n "enum": [\n "renderer-to-main",\n "main-to-renderer",\n "main-to-utility",\n "utility-to-main"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "error": {\n "type": "boolean"\n },\n "event": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "frame": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "enum": [\n "main-handler-invocation",\n "main-event-invocation",\n "utility-process-fork",\n "utility-process-message",\n "ipc-main-to-renderer",\n "ipc-utility-to-main",\n "ipc-renderer-send",\n "ipc-renderer-invoke",\n "ipc-renderer-post-message",\n "app-lifecycle",\n "window-lifecycle",\n "web-contents-lifecycle",\n "navigation",\n "shell-attempt",\n "process-lifecycle",\n "permission",\n "popup-attempt",\n "download",\n "protocol",\n "preload",\n "native-addon",\n "updater",\n "error"\n ],\n "type": "string"\n },\n "phase": {\n "enum": [\n "attempted",\n "completed",\n "blocked",\n "failed",\n "observed"\n ],\n "type": "string"\n },\n "process_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "receiver": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "result_shape": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sender": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "source": {\n "type": "string"\n },\n "target": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "sequence",\n "correlation_id",\n "kind",\n "event",\n "phase",\n "channel",\n "direction",\n "sender",\n "receiver",\n "frame",\n "target",\n "argument_shapes",\n "result_shape",\n "process_type",\n "source",\n "capture_method",\n "artifact_path",\n "artifact_sha256",\n "error"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "events",\n "observed"\n ],\n "type": "object"\n },\n "windows": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "destroyed": {\n "type": "boolean"\n },\n "title": {\n "type": "string"\n },\n "url": {\n "type": "string"\n },\n "visible": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "web_contents_id": {\n "type": "string"\n },\n "window_id": {\n "type": "string"\n }\n },\n "required": [\n "window_id",\n "web_contents_id",\n "url",\n "title",\n "visible",\n "destroyed"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "application",\n "actions",\n "windows",\n "processes",\n "ipc",\n "timeline",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Capture Electron Scenario"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "List every eligible Node/Electron V8 Inspector target from an explicit literal-loopback endpoint. Node discovery file locations that cannot be verified are preserved as unresolved reported URLs; they do not prove filesystem identity. Listing does not attach to targets.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "inspector_endpoint": "http://127.0.0.1:9229"\n }\n ],\n "properties": {\n "inspector_endpoint": {\n "description": "Value for inspector endpoint.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "inspector_endpoint"\n ],\n "type": "object"\n },\n "kind": "runtime-provider",\n "name": "list_javascript_runtime_targets",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "excluded": {\n "additionalProperties": false,\n "properties": {\n "unconnectable": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unsupported_location": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unsupported_location",\n "unconnectable"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "runtime": {\n "additionalProperties": false,\n "properties": {\n "product": {\n "minLength": 1,\n "type": "string"\n },\n "protocol_version": {\n "minLength": 1,\n "type": "string"\n },\n "v8_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "product",\n "protocol_version",\n "v8_version"\n ],\n "type": "object"\n },\n "targets": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "location": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "const": "scope-fallback",\n "type": "string"\n },\n "file_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "file_path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "origin": {\n "minLength": 1,\n "type": "string"\n },\n "sanitized_url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "origin",\n "sanitized_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "builtin",\n "type": "string"\n },\n "specifier": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "specifier"\n ],\n "type": "object"\n }\n ]\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "unresolved",\n "type": "string"\n },\n "reason": {\n "const": "unverifiable-file-location",\n "type": "string"\n },\n "reported_url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "reported_url",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "protocol_type": {\n "minLength": 1,\n "type": "string"\n },\n "target_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "protocol_type",\n "attached",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "runtime",\n "targets",\n "excluded",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "List Javascript Runtime Targets"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Attach passively to one Node/Electron V8 Inspector target by supplying its loopback Inspector endpoint and target ID. The provider rechecks target presence and attachability; a Node discovery file location may remain unresolved. Loaded script locations are validated independently from Debugger.scriptParsed. Captures script and Runtime execution-context metadata. REA never evaluates, pauses, resumes, reads source, or instruments the target; require/import edges, EventEmitter activity, and Electron IPC remain explicit unknowns. Reconcile the result with static Application Graph Evidence using reconcile_javascript_runtime. The selected endpoint exposes every target it serves to this tool.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "inspector_endpoint": "http://127.0.0.1:9229",\n "target_id": "TARGET_ID_FROM_LIST_JAVASCRIPT_RUNTIME_TARGETS"\n }\n ],\n "properties": {\n "inspector_endpoint": {\n "description": "Value for inspector endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "observation_ms": {\n "default": 100,\n "description": "Value for observation ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "runtime_kind": {\n "description": "Value for runtime kind.",\n "enum": [\n "node",\n "electron-main",\n "electron-preload",\n "electron-renderer"\n ],\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "inspector_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n "kind": "runtime-provider",\n "name": "observe_javascript_runtime",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "capture": {\n "additionalProperties": false,\n "properties": {\n "events_dropped": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "events_observed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "events_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "metadata_bytes_retained": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "observation_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "type": "boolean"\n },\n "truncation_reasons": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "observation_ms",\n "events_observed",\n "events_retained",\n "events_dropped",\n "metadata_bytes_retained",\n "truncated",\n "truncation_reasons"\n ],\n "type": "object"\n },\n "directly_observed": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "execution_contexts": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "context_key": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "origin": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "state": {\n "enum": [\n "created",\n "destroyed",\n "cleared"\n ],\n "type": "string"\n }\n },\n "required": [\n "context_key",\n "state",\n "name",\n "origin"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "runtime": {\n "additionalProperties": false,\n "properties": {\n "product": {\n "minLength": 1,\n "type": "string"\n },\n "protocol_version": {\n "minLength": 1,\n "type": "string"\n },\n "v8_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "product",\n "protocol_version",\n "v8_version"\n ],\n "type": "object"\n },\n "scripts": {\n "additionalProperties": false,\n "properties": {\n "excluded": {\n "additionalProperties": false,\n "properties": {\n "invalid_protocol_value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unsupported_location": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unsupported_location",\n "invalid_protocol_value"\n ],\n "type": "object"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "cdp_hash": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "execution_context_key": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_module": {\n "type": "boolean"\n },\n "length": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "const": "scope-fallback",\n "type": "string"\n },\n "file_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "file_path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "origin": {\n "minLength": 1,\n "type": "string"\n },\n "sanitized_url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "origin",\n "sanitized_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "builtin",\n "type": "string"\n },\n "specifier": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "specifier"\n ],\n "type": "object"\n }\n ]\n },\n "script_key": {\n "pattern": "^v8_script_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "observed-loaded",\n "type": "string"\n }\n },\n "required": [\n "script_key",\n "location",\n "execution_context_key",\n "cdp_hash",\n "length",\n "is_module",\n "status"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "observed_total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "items",\n "observed_total",\n "excluded"\n ],\n "type": "object"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "attached": {\n "type": "boolean"\n },\n "location": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "const": "scope-fallback",\n "type": "string"\n },\n "file_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "file_path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "origin": {\n "minLength": 1,\n "type": "string"\n },\n "sanitized_url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "origin",\n "sanitized_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "builtin",\n "type": "string"\n },\n "specifier": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "specifier"\n ],\n "type": "object"\n }\n ]\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "unresolved",\n "type": "string"\n },\n "reason": {\n "const": "unverifiable-file-location",\n "type": "string"\n },\n "reported_url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "reported_url",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "protocol_type": {\n "minLength": 1,\n "type": "string"\n },\n "runtime_kind": {\n "anyOf": [\n {\n "enum": [\n "node",\n "electron-main",\n "electron-preload",\n "electron-renderer"\n ],\n "type": "string"\n },\n {\n "const": "unknown",\n "type": "string"\n }\n ]\n },\n "runtime_kind_authority": {\n "enum": [\n "caller-declared-unverified",\n "not-declared"\n ],\n "type": "string"\n },\n "target_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "protocol_type",\n "attached",\n "location",\n "runtime_kind",\n "runtime_kind_authority"\n ],\n "type": "object"\n },\n "unavailable_without_instrumentation": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "unknowns": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "runtime",\n "target",\n "capture",\n "scripts",\n "execution_contexts",\n "directly_observed",\n "unavailable_without_instrumentation",\n "unknowns",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Observe Javascript Runtime"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Trace a typed literal seed through every reachable part of an authenticated JavaScript Application Graph supplied as inline Evidence or exact same-session retained references. Original static, native, passive-runtime, inferred, and unknown authorities remain distinct; native addon handoffs never open a provider or execute the application.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "application": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "format": "directory",\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [],\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "limitations": [],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "inventory_graph_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "inventory_manifest_id": "agm_2222222222222222222222222222222222222222222222222222222222222222",\n "limitations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "semantic_graph": {\n "application_graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "coverage": {\n "families": [\n {\n "family": "boundary",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "call-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "child-process",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "closure",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "configuration",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "data-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "event",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "object-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "promise-ownership",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "request",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "resource-lifecycle",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "timer",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n }\n ],\n "limits": [],\n "omitted_nodes": 0,\n "omitted_relations": 0,\n "status": "unknown",\n "truncated": false\n },\n "fingerprints": [],\n "graph_id": "jsrg_08c925d2bf4ef56d770e0e67901d5bdfb610744cf001a50d36c9bb6dcb195508",\n "limitations": [\n "Semantic relations are unavailable in this minimal example."\n ],\n "nodes": [\n {\n "application_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "function_node_id": null,\n "identifier_strategy": {\n "stability": "artifact-version",\n "strategy": "semantic-content-sha256"\n },\n "identity": {\n "artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "module_path": "renderer.js",\n "role_key": "example-module",\n "source_range": null\n },\n "kind": "module",\n "label": "renderer.js",\n "node_id": "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a",\n "properties": {}\n }\n ],\n "relations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "root_node_ids": [\n "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a"\n ],\n "schema": "JavaScriptSemanticRelationGraph",\n "unknowns": []\n },\n "statistics": {\n "findings": 0,\n "invalid_utf8_files": 0,\n "modules": 0,\n "nested_asar_containers": 0,\n "parse_failures": 0,\n "parsed_javascript_files": 1,\n "relevant_files": 1,\n "text_bytes_read": 29,\n "truncated_scopes": 0,\n "visited_ast_nodes": 1\n },\n "summary": {\n "browser_windows": 0,\n "context_bridge_apis": 0,\n "explicit_web_preferences": 0,\n "exposed_api_members": 0,\n "ipc": {\n "ambiguous_renderer_transmissions": 0,\n "dynamic_channel_operations": 0,\n "literal_channels": 0,\n "main_handlers": 0,\n "operations": 0,\n "paired_renderer_transmissions": 0,\n "renderer_listeners": 0,\n "renderer_transmissions": 0,\n "unpaired_literal_renderer_transmissions": 0\n },\n "native_addon_bindings": 0,\n "preload_entrypoints": 0,\n "resolved_native_addon_bindings": 0,\n "resolved_utility_entrypoints": 0,\n "sender_validation_observations": 0,\n "utility_processes": 0\n }\n },\n "operation": "analyze_javascript_application",\n "parameters": {},\n "predicate_type": "rea.javascript-application-analysis",\n "provider": {\n "id": "rea-javascript-application",\n "name": "REA JavaScript application analyzer",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "directory",\n "local_path": "/Applications/Example.app/Contents/Resources/app",\n "name": "app"\n }\n },\n "direction": "both",\n "seed": {\n "case_sensitive": false,\n "kind": "module",\n "match": "exact",\n "value": "renderer.js"\n }\n },\n {\n "application": {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "kind": "retained-evidence"\n },\n "direction": "both",\n "seed": {\n "case_sensitive": false,\n "kind": "module",\n "match": "exact",\n "value": "renderer.js"\n }\n }\n ],\n "properties": {\n "application": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Value for application."\n },\n "direction": {\n "default": "both",\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "outgoing",\n "incoming",\n "both"\n ],\n "type": "string"\n },\n "native_observations": {\n "default": [],\n "description": "Value for native observations.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "seed": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "case_sensitive": {\n "default": false,\n "description": "Whether text matching distinguishes uppercase and lowercase.",\n "type": "boolean"\n },\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "node-id",\n "route",\n "string",\n "api",\n "channel",\n "module",\n "native-export"\n ],\n "type": "string"\n },\n "match": {\n "anyOf": [\n {\n "enum": [\n "exact",\n "contains"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for match."\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "case_sensitive": {\n "default": false,\n "description": "Whether text matching distinguishes uppercase and lowercase.",\n "type": "boolean"\n },\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "route",\n "string",\n "api",\n "channel",\n "module",\n "native-export"\n ],\n "type": "string"\n },\n "match": {\n "const": "exact",\n "description": "Value for match.",\n "type": "string"\n },\n "value": {\n "const": "",\n "description": "Value for value.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value",\n "match"\n ],\n "type": "object"\n }\n ],\n "description": "Value for seed."\n }\n },\n "required": [\n "seed",\n "application"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "trace_application_feature",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "source_graph_status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "complete-within-source",\n "partial",\n "no-match"\n ],\n "type": "string"\n },\n "total_seed_matches": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "source_graph_status",\n "total_seed_matches"\n ],\n "type": "object"\n },\n "direction": {\n "enum": [\n "outgoing",\n "incoming",\n "both"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "graph": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "edge_id": {\n "pattern": "^jag_edge_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "contains",\n "loads",\n "imports",\n "maps_to",\n "exposes",\n "sends",\n "invokes",\n "handles",\n "calls",\n "persists_to",\n "observed_as",\n "changed_from"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "properties",\n "evidence",\n "edge_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "identity": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "global-exact",\n "type": "string"\n },\n "strategy": {\n "const": "content-digest",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "original_source": {\n "type": "string"\n },\n "source_map_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "stability": {\n "const": "source-map-exact",\n "type": "string"\n },\n "strategy": {\n "const": "source-map-original",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "source_map_sha256",\n "original_source",\n "source_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "canonical-path",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "minLength": 1,\n "type": "string"\n },\n "basis": {\n "items": {\n "enum": [\n "syntax-tree",\n "control-flow",\n "imports",\n "exports",\n "literals",\n "api-usage"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "cross-version-inference",\n "type": "string"\n },\n "strategy": {\n "const": "structural-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "algorithm",\n "fingerprint_sha256",\n "basis"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "key": {\n "type": "string"\n },\n "namespace": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "artifact-local-key",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "namespace",\n "key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_key": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "capture-only",\n "type": "string"\n },\n "strategy": {\n "const": "runtime-instance",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "capture_sha256",\n "runtime_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observation_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "observation-only",\n "type": "string"\n },\n "strategy": {\n "const": "observation-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "observation_sha256",\n "scope"\n ],\n "type": "object"\n }\n ]\n },\n "kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "observations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "observation-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "observation_id": {\n "pattern": "^jag_observation_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "label",\n "properties",\n "evidence",\n "observation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "kind",\n "identity",\n "observations"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "root_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "schema": {\n "const": "JavaScriptApplicationGraph",\n "type": "string"\n }\n },\n "required": [\n "schema",\n "root_node_ids",\n "nodes",\n "edges",\n "coverage",\n "limitations",\n "graph_id"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "native_handoffs": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "export_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "native_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "providers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "recommended_tools": {\n "items": {\n "enum": [\n "open_binary",\n "binary_overview",\n "search_procedures",\n "analyze_function",\n "xrefs"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "requested_exports": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "evidence-linked",\n "requires-provider-analysis"\n ],\n "type": "string"\n }\n },\n "required": [\n "native_node_id",\n "artifact_sha256",\n "artifact_path",\n "export_node_ids",\n "requested_exports",\n "status",\n "providers",\n "evidence_ids",\n "recommended_tools"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "paths": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "authorities": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "contains_inference": {\n "type": "boolean"\n },\n "edge_ids": {\n "items": {\n "pattern": "^jag_edge_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "end_kind": {\n "minLength": 1,\n "type": "string"\n },\n "end_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "path_id": {\n "pattern": "^jatp_[a-f0-9]{64}$",\n "type": "string"\n },\n "start_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path_id",\n "start_node_id",\n "end_node_id",\n "end_kind",\n "node_ids",\n "edge_ids",\n "authorities",\n "contains_inference"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "seed": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "case_sensitive": {\n "default": false,\n "type": "boolean"\n },\n "kind": {\n "enum": [\n "node-id",\n "route",\n "string",\n "api",\n "channel",\n "module",\n "native-export"\n ],\n "type": "string"\n },\n "match": {\n "anyOf": [\n {\n "enum": [\n "exact",\n "contains"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "default": null\n },\n "value": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value",\n "match",\n "case_sensitive"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "case_sensitive": {\n "default": false,\n "type": "boolean"\n },\n "kind": {\n "enum": [\n "route",\n "string",\n "api",\n "channel",\n "module",\n "native-export"\n ],\n "type": "string"\n },\n "match": {\n "const": "exact",\n "type": "string"\n },\n "value": {\n "const": "",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value",\n "match",\n "case_sensitive"\n ],\n "type": "object"\n }\n ]\n },\n "seed_matches": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "node-id",\n "label",\n "identity",\n "property"\n ],\n "type": "string"\n },\n "field": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "kind",\n "basis",\n "field"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "source_graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "inferred_facts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "matched_seeds": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "native_handoffs": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "observed_facts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "terminal_paths": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "traced_edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "traced_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unavailable_facts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown_facts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "matched_seeds",\n "traced_nodes",\n "traced_edges",\n "terminal_paths",\n "native_handoffs",\n "observed_facts",\n "inferred_facts",\n "unknown_facts",\n "unavailable_facts"\n ],\n "type": "object"\n },\n "trace_id": {\n "pattern": "^jatr_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "trace_id",\n "source_evidence_id",\n "source_graph_id",\n "seed",\n "direction",\n "seed_matches",\n "graph",\n "paths",\n "native_handoffs",\n "summary",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Application Feature"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Trace static JavaScript data-flow, direct call/return, and closure relations from inline application Evidence or an exact same-session retained reference. Dynamic or unsupported semantics remain explicit unknowns; static reachability never claims runtime execution.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "application": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "format": "directory",\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [],\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "limitations": [],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "inventory_graph_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "inventory_manifest_id": "agm_2222222222222222222222222222222222222222222222222222222222222222",\n "limitations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "semantic_graph": {\n "application_graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "coverage": {\n "families": [\n {\n "family": "boundary",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "call-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "child-process",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "closure",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "configuration",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "data-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "event",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "object-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "promise-ownership",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "request",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "resource-lifecycle",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "timer",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n }\n ],\n "limits": [],\n "omitted_nodes": 0,\n "omitted_relations": 0,\n "status": "unknown",\n "truncated": false\n },\n "fingerprints": [],\n "graph_id": "jsrg_08c925d2bf4ef56d770e0e67901d5bdfb610744cf001a50d36c9bb6dcb195508",\n "limitations": [\n "Semantic relations are unavailable in this minimal example."\n ],\n "nodes": [\n {\n "application_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "function_node_id": null,\n "identifier_strategy": {\n "stability": "artifact-version",\n "strategy": "semantic-content-sha256"\n },\n "identity": {\n "artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "module_path": "renderer.js",\n "role_key": "example-module",\n "source_range": null\n },\n "kind": "module",\n "label": "renderer.js",\n "node_id": "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a",\n "properties": {}\n }\n ],\n "relations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "root_node_ids": [\n "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a"\n ],\n "schema": "JavaScriptSemanticRelationGraph",\n "unknowns": []\n },\n "statistics": {\n "findings": 0,\n "invalid_utf8_files": 0,\n "modules": 0,\n "nested_asar_containers": 0,\n "parse_failures": 0,\n "parsed_javascript_files": 1,\n "relevant_files": 1,\n "text_bytes_read": 29,\n "truncated_scopes": 0,\n "visited_ast_nodes": 1\n },\n "summary": {\n "browser_windows": 0,\n "context_bridge_apis": 0,\n "explicit_web_preferences": 0,\n "exposed_api_members": 0,\n "ipc": {\n "ambiguous_renderer_transmissions": 0,\n "dynamic_channel_operations": 0,\n "literal_channels": 0,\n "main_handlers": 0,\n "operations": 0,\n "paired_renderer_transmissions": 0,\n "renderer_listeners": 0,\n "renderer_transmissions": 0,\n "unpaired_literal_renderer_transmissions": 0\n },\n "native_addon_bindings": 0,\n "preload_entrypoints": 0,\n "resolved_native_addon_bindings": 0,\n "resolved_utility_entrypoints": 0,\n "sender_validation_observations": 0,\n "utility_processes": 0\n }\n },\n "operation": "analyze_javascript_application",\n "parameters": {},\n "predicate_type": "rea.javascript-application-analysis",\n "provider": {\n "id": "rea-javascript-application",\n "name": "REA JavaScript application analyzer",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "directory",\n "local_path": "/Applications/Example.app/Contents/Resources/app",\n "name": "app"\n }\n },\n "query": {\n "direction": "backward-provenance",\n "seed": {\n "kind": "semantic-node",\n "node_id": "jsrg_node_0000000000000000000000000000000000000000000000000000000000000000"\n }\n }\n }\n ],\n "properties": {\n "application": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Value for application."\n },\n "query": {\n "additionalProperties": false,\n "description": "Non-empty feature or text query to investigate.",\n "properties": {\n "allowed_relations": {\n "description": "Value for allowed relations.",\n "items": {\n "enum": [\n "acquires",\n "aggregates",\n "aliases",\n "argument-to-parameter",\n "awaits",\n "calls",\n "cancels-timer",\n "captures",\n "chains",\n "coerces",\n "connects-stdio",\n "consumed-by",\n "constructs-request",\n "creates-promise",\n "defaults",\n "defines",\n "destructures",\n "detaches-task",\n "dispatches-candidate",\n "forwards-signal",\n "listens-error",\n "listens-exit",\n "owns",\n "overrides",\n "parses",\n "reads",\n "reads-argv",\n "reads-config",\n "reads-environment",\n "reads-property",\n "registers-listener",\n "releases",\n "removes-listener",\n "returns-task",\n "returns-to-call",\n "schedules-timer",\n "spawns",\n "spreads",\n "supplies-argv",\n "supplies-env",\n "supplies-request-field",\n "validates",\n "writes",\n "writes-property"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "direction": {\n "description": "Direction in which to traverse or compare relationships.",\n "enum": [\n "backward-provenance",\n "forward-influence",\n "callers",\n "ownership"\n ],\n "type": "string"\n },\n "expected": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "classes": {\n "description": "Value for classes.",\n "items": {\n "enum": [\n "module",\n "function",\n "parameter",\n "binding",\n "expression",\n "literal",\n "property-slot",\n "call-site",\n "return-site",\n "closure-capture",\n "promise",\n "task",\n "event",\n "listener",\n "timer",\n "child-process",\n "stdio",\n "signal",\n "config-source",\n "request",\n "response",\n "boundary",\n "resource",\n "unknown-frontier"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "role": {\n "description": "Value for role.",\n "enum": [\n "source",\n "sink"\n ],\n "type": "string"\n }\n },\n "required": [\n "role",\n "classes"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "default": null,\n "description": "Value for expected."\n },\n "include_ambiguous_dynamic_edges": {\n "default": false,\n "description": "Whether to include ambiguous dynamic edges in the result.",\n "type": "boolean"\n },\n "seed": {\n "description": "Value for seed.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "semantic-node",\n "description": "Value for kind.",\n "type": "string"\n },\n "node_id": {\n "description": "Value for node id.",\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "application-node",\n "description": "Value for kind.",\n "type": "string"\n },\n "node_id": {\n "description": "Value for node id.",\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "literal",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for value."\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "fingerprint_sha256": {\n "description": "Exact SHA-256 digest of fingerprint.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "function",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "fingerprint_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "property",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "endpoint",\n "description": "Value for kind.",\n "type": "string"\n },\n "value": {\n "description": "Value for value.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "event",\n "description": "Value for kind.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "field": {\n "description": "Value for field.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "boundary-field",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "field"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "seed",\n "direction"\n ],\n "type": "object"\n }\n },\n "required": [\n "application",\n "query"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "trace_javascript_semantics",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "complete",\n "partial",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n "direction": {\n "enum": [\n "backward-provenance",\n "forward-influence",\n "callers",\n "ownership"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 1,\n "minItems": 1,\n "type": "array"\n },\n "expected_match_node_ids": {\n "items": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "application_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "function_node_id": {\n "anyOf": [\n {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "identity": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "role_key": {\n "minLength": 1,\n "type": "string"\n },\n "source_range": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "artifact_sha256",\n "module_path",\n "source_range",\n "role_key"\n ],\n "type": "object"\n },\n "kind": {\n "enum": [\n "module",\n "function",\n "parameter",\n "binding",\n "expression",\n "literal",\n "property-slot",\n "call-site",\n "return-site",\n "closure-capture",\n "promise",\n "task",\n "event",\n "listener",\n "timer",\n "child-process",\n "stdio",\n "signal",\n "config-source",\n "request",\n "response",\n "boundary",\n "resource",\n "unknown-frontier"\n ],\n "type": "string"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "kind",\n "identity",\n "function_node_id",\n "application_node_ids",\n "label",\n "properties",\n "evidence",\n "node_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "query_id": {\n "pattern": "^jsrq_[a-f0-9]{64}$",\n "type": "string"\n },\n "relations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "acquires",\n "aggregates",\n "aliases",\n "argument-to-parameter",\n "awaits",\n "calls",\n "cancels-timer",\n "captures",\n "chains",\n "coerces",\n "connects-stdio",\n "consumed-by",\n "constructs-request",\n "creates-promise",\n "defaults",\n "defines",\n "destructures",\n "detaches-task",\n "dispatches-candidate",\n "forwards-signal",\n "listens-error",\n "listens-exit",\n "owns",\n "overrides",\n "parses",\n "reads",\n "reads-argv",\n "reads-config",\n "reads-environment",\n "reads-property",\n "registers-listener",\n "releases",\n "removes-listener",\n "returns-task",\n "returns-to-call",\n "schedules-timer",\n "spawns",\n "spreads",\n "supplies-argv",\n "supplies-env",\n "supplies-request-field",\n "validates",\n "writes",\n "writes-property"\n ],\n "type": "string"\n },\n "relation_id": {\n "pattern": "^jsrg_relation_[a-f0-9]{64}$",\n "type": "string"\n },\n "resolution": {\n "enum": [\n "resolved",\n "candidate"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "resolution",\n "properties",\n "evidence",\n "relation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "seed": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "semantic-node",\n "type": "string"\n },\n "node_id": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "application-node",\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "literal",\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "function",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "fingerprint_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "property",\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "endpoint",\n "type": "string"\n },\n "value": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "event",\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "field": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "boundary-field",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "field"\n ],\n "type": "object"\n }\n ]\n },\n "seed_node_ids": {\n "items": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "source_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "source_graph_id": {\n "pattern": "^jsrg_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "found",\n "no-match",\n "ambiguous",\n "partial",\n "unsupported"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "relevant_unknowns": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total_seed_matches": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "traversed_functions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "traversed_modules": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "traversed_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "traversed_relations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total_seed_matches",\n "traversed_nodes",\n "traversed_relations",\n "traversed_functions",\n "traversed_modules",\n "relevant_unknowns"\n ],\n "type": "object"\n },\n "unknowns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "candidate_node_ids": {\n "items": {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "family": {\n "enum": [\n "boundary",\n "call-flow",\n "child-process",\n "closure",\n "configuration",\n "data-flow",\n "event",\n "object-flow",\n "promise-ownership",\n "request",\n "resource-lifecycle",\n "timer"\n ],\n "type": "string"\n },\n "node_id": {\n "anyOf": [\n {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "ambiguous-target",\n "bound-reached",\n "dynamic-call",\n "dynamic-property",\n "dynamic-scope",\n "eval-or-generated-code",\n "incomplete-module",\n "missing-source",\n "unsupported-syntax"\n ],\n "type": "string"\n },\n "relation_kinds": {\n "items": {\n "enum": [\n "acquires",\n "aggregates",\n "aliases",\n "argument-to-parameter",\n "awaits",\n "calls",\n "cancels-timer",\n "captures",\n "chains",\n "coerces",\n "connects-stdio",\n "consumed-by",\n "constructs-request",\n "creates-promise",\n "defaults",\n "defines",\n "destructures",\n "detaches-task",\n "dispatches-candidate",\n "forwards-signal",\n "listens-error",\n "listens-exit",\n "owns",\n "overrides",\n "parses",\n "reads",\n "reads-argv",\n "reads-config",\n "reads-environment",\n "reads-property",\n "registers-listener",\n "releases",\n "removes-listener",\n "returns-task",\n "returns-to-call",\n "schedules-timer",\n "spawns",\n "spreads",\n "supplies-argv",\n "supplies-env",\n "supplies-request-field",\n "validates",\n "writes",\n "writes-property"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^jsrg_unknown_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "family",\n "relation_kinds",\n "reason",\n "detail",\n "candidate_node_ids",\n "evidence",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "query_id",\n "source_graph_id",\n "seed",\n "direction",\n "status",\n "seed_node_ids",\n "nodes",\n "relations",\n "unknowns",\n "expected_match_node_ids",\n "summary",\n "coverage",\n "limitations",\n "source_evidence_id",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Javascript Semantics"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare two authenticated JavaScript Application Graph versions supplied as inline Evidence or exact same-session retained references. Uses unique-only exact digest, module source digest, source-map identity, structural fingerprint, and non-module semantic-key tiers. Reports added, removed, changed, ambiguous, and unknown entities plus the complete matching changed_from graph without fuzzy or module-ordinal pairing.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "format": "directory",\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [],\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "limitations": [],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "inventory_graph_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "inventory_manifest_id": "agm_2222222222222222222222222222222222222222222222222222222222222222",\n "limitations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "semantic_graph": {\n "application_graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "coverage": {\n "families": [\n {\n "family": "boundary",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "call-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "child-process",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "closure",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "configuration",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "data-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "event",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "object-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "promise-ownership",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "request",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "resource-lifecycle",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "timer",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n }\n ],\n "limits": [],\n "omitted_nodes": 0,\n "omitted_relations": 0,\n "status": "unknown",\n "truncated": false\n },\n "fingerprints": [],\n "graph_id": "jsrg_08c925d2bf4ef56d770e0e67901d5bdfb610744cf001a50d36c9bb6dcb195508",\n "limitations": [\n "Semantic relations are unavailable in this minimal example."\n ],\n "nodes": [\n {\n "application_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "function_node_id": null,\n "identifier_strategy": {\n "stability": "artifact-version",\n "strategy": "semantic-content-sha256"\n },\n "identity": {\n "artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "module_path": "renderer.js",\n "role_key": "example-module",\n "source_range": null\n },\n "kind": "module",\n "label": "renderer.js",\n "node_id": "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a",\n "properties": {}\n }\n ],\n "relations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "root_node_ids": [\n "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a"\n ],\n "schema": "JavaScriptSemanticRelationGraph",\n "unknowns": []\n },\n "statistics": {\n "findings": 0,\n "invalid_utf8_files": 0,\n "modules": 0,\n "nested_asar_containers": 0,\n "parse_failures": 0,\n "parsed_javascript_files": 1,\n "relevant_files": 1,\n "text_bytes_read": 29,\n "truncated_scopes": 0,\n "visited_ast_nodes": 1\n },\n "summary": {\n "browser_windows": 0,\n "context_bridge_apis": 0,\n "explicit_web_preferences": 0,\n "exposed_api_members": 0,\n "ipc": {\n "ambiguous_renderer_transmissions": 0,\n "dynamic_channel_operations": 0,\n "literal_channels": 0,\n "main_handlers": 0,\n "operations": 0,\n "paired_renderer_transmissions": 0,\n "renderer_listeners": 0,\n "renderer_transmissions": 0,\n "unpaired_literal_renderer_transmissions": 0\n },\n "native_addon_bindings": 0,\n "preload_entrypoints": 0,\n "resolved_native_addon_bindings": 0,\n "resolved_utility_entrypoints": 0,\n "sender_validation_observations": 0,\n "utility_processes": 0\n }\n },\n "operation": "analyze_javascript_application",\n "parameters": {},\n "predicate_type": "rea.javascript-application-analysis",\n "provider": {\n "id": "rea-javascript-application",\n "name": "REA JavaScript application analyzer",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "directory",\n "local_path": "/Applications/Example.app/Contents/Resources/app",\n "name": "app"\n }\n },\n "right": {\n "authority": "analyst-inference",\n "confidence": "inferred",\n "environment": null,\n "evidence_id": "ev_c2754cf75e286b839ed825f247062067a6472acb1e7b6ce0f9aaceec6448d6d7",\n "evidence_links": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54"\n ],\n "limitations": [\n "Artifact identity is unavailable for this observation.",\n "A static module resident in an observed bundle is not reported as executed.",\n "Caller-declared file and URL mappings are inference inputs and do not expand CDP origin or filesystem-root authority.",\n "Digest equality establishes byte identity but not code reachability, module initialization, or feature execution.",\n "Not-observed means absent from the bounded captures in scope, never globally unloaded.",\n "Only caller-supplied, semantically verified Evidence records were reconciled.",\n "Source-map authority remains separate from passive runtime authority."\n ],\n "locations": [],\n "normalized_result": {\n "coverage": {\n "omitted_graph_items": 0,\n "omitted_reconciliation_items": 0,\n "omitted_runtime_entities": 0,\n "omitted_static_load_states": 0,\n "status": "complete-within-inputs",\n "truncated": false\n },\n "evidence_links": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54"\n ],\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [\n {\n "edge_id": "jag_edge_3799eb1c2b0f224013a0edba8911a8b9edabd5fa6da2f893a6cfa24a899c9463",\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "cross-layer-reconciliation",\n "confidence": "high",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-runtime-reconciliation",\n "operation": "reconcile_javascript_runtime",\n "version": "1"\n },\n "limitations": [\n "Equal content digests establish byte identity, not execution causality.",\n "Passive script parsing or target presence does not prove feature execution."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "target_key": "example-target"\n }\n },\n "state": "inferred"\n },\n "identifier_strategy": {\n "stability": "relationship-exact",\n "strategy": "semantic-content-sha256"\n },\n "properties": {\n "basis": "content-and-location",\n "reconciliation_id": "jrr_item_ea9d751c4bf3a2234af56b80d5ac8a1f953da9a9477cd46586a62934ff4a5fb2",\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753"\n },\n "relation": "observed_as",\n "source_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "target_node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e"\n },\n {\n "edge_id": "jag_edge_f3c06b02c5ac77056b5d2a9f8de834ea0a9123b01dc369a011e3adc4a535338b",\n "evidence": {\n "artifact": {\n "available": false,\n "detail": "The relationship is runtime metadata, not standalone bytes.",\n "reason": "not-applicable"\n },\n "authority": "passive-cdp-runtime",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-cdp-electron",\n "operation": "inspect_electron_page",\n "version": "1"\n },\n "limitations": [\n "The relationship was observed only within one bounded passive CDP capture."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "target_key": "example-target"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "relationship-exact",\n "strategy": "semantic-content-sha256"\n },\n "properties": {\n "basis": "passive-cdp-observation"\n },\n "relation": "loads",\n "source_node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "target_node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e"\n }\n ],\n "graph_id": "jag_0536a9584eb91a16f78f71e8bef4cffc3105df18098fe65a7b114add520203ca",\n "limitations": [\n "Cross-layer observed_as edges are inferences; digest equality proves byte identity only.",\n "Runtime presence is bounded to passive capture windows and does not prove feature execution.",\n "Source-map relationships retain their original static authority and are not promoted into runtime matches."\n ],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n },\n {\n "identity": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "runtime_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "stability": "capture-only",\n "strategy": "runtime-instance"\n },\n "kind": "runtime-script-instance",\n "node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "passive-cdp-runtime",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-cdp-electron",\n "operation": "inspect_electron_page",\n "version": "1"\n },\n "limitations": [\n "The entity was observed only within one bounded passive CDP capture."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "target_key": "example-target"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "/Applications/Example.app/Contents/Resources/app/renderer.js",\n "observation_id": "jag_observation_9315403b875ea70c310909daf797780ab9045ff8f4803db38a9184e4f0704f11",\n "properties": {\n "cdp_hash": "example",\n "is_module": true,\n "language": "JavaScript",\n "length": 29,\n "location": "/Applications/Example.app/Contents/Resources/app/renderer.js",\n "source_sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n }\n }\n ]\n },\n {\n "identity": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "runtime_key": "example-target",\n "stability": "capture-only",\n "strategy": "runtime-instance"\n },\n "kind": "target",\n "node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "available": false,\n "detail": "This runtime entity has no standalone artifact bytes.",\n "reason": "not-applicable"\n },\n "authority": "passive-cdp-runtime",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-cdp-electron",\n "operation": "inspect_electron_page",\n "version": "1"\n },\n "limitations": [\n "The entity was observed only within one bounded passive CDP capture."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": null,\n "target_key": "example-target"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "Example",\n "observation_id": "jag_observation_844a03a29af96a0bcd4625e0377b67c439af68a462c5fa83009c7c27283c5dee",\n "properties": {\n "attached": false,\n "location": "/Applications/Example.app/Contents/Resources/app/index.html",\n "runtime_type": "page"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "limitations": [\n "A static module resident in an observed bundle is not reported as executed.",\n "Caller-declared file and URL mappings are inference inputs and do not expand CDP origin or filesystem-root authority.",\n "Digest equality establishes byte identity but not code reachability, module initialization, or feature execution.",\n "Not-observed means absent from the bounded captures in scope, never globally unloaded.",\n "Only caller-supplied, semantically verified Evidence records were reconciled.",\n "Source-map authority remains separate from passive runtime authority."\n ],\n "reconciliation_id": "jrr_ec50425a5a5a5cb85aa40f13f72dcba54a8ab16170d9421a0277fbdf0fa4c9eb",\n "reconciliations": [\n {\n "basis": "none",\n "candidate_static_count": 0,\n "candidate_static_nodes": [],\n "confidence": "unknown",\n "entity_kind": "target",\n "reason": "no-static-content-match",\n "reconciliation_id": "jrr_item_abd5635b06340f082ef46bff8802a361c7b30011085f1c9e7437b29b451c1e5e",\n "runtime_evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "runtime_node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "static_layer_id": null,\n "static_node_id": null,\n "status": "unmatched"\n },\n {\n "basis": "content-and-location",\n "candidate_static_count": 1,\n "candidate_static_nodes": [\n {\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "static_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n }\n ],\n "confidence": "high",\n "entity_kind": "script",\n "reason": "content-and-location-match",\n "reconciliation_id": "jrr_item_ea9d751c4bf3a2234af56b80d5ac8a1f953da9a9477cd46586a62934ff4a5fb2",\n "runtime_evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "runtime_node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e",\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "static_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "status": "matched"\n }\n ],\n "runtime_captures": [\n {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "frames": 0,\n "kind": "electron",\n "scripts": 1,\n "scripts_complete_within_scope": true,\n "target_key": "example-target",\n "target_location": "/Applications/Example.app/Contents/Resources/app/index.html",\n "target_node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "workers": 0\n }\n ],\n "source_map_authority": {\n "limitation": "Source-map declarations and original-source content retain separate static authority and are not used as primary runtime byte matches.",\n "runtime_script_declarations": 0,\n "static_layer_count": 1,\n "used_for_primary_matching": false\n },\n "static_layers": [\n {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "format": "directory",\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "role": "application",\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "runtime_mappings": []\n }\n ],\n "static_load_states": [\n {\n "kind": "javascript-asset",\n "reason": "runtime-script-correspondence",\n "runtime_node_ids": [\n "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e"\n ],\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "static_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "status": "loaded"\n }\n ],\n "summary": {\n "ambiguous": 0,\n "matched": 1,\n "runtime_frames": 0,\n "runtime_scripts": 1,\n "runtime_targets": 1,\n "runtime_workers": 0,\n "static_loaded": 1,\n "static_not_observed": 0,\n "static_resident": 0,\n "static_unknown": 0,\n "unknown": 0,\n "unmatched": 1\n }\n },\n "operation": "reconcile_javascript_runtime",\n "parameters": {\n "runtime_evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "static_layers": [\n {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "role": "application",\n "runtime_mappings": []\n }\n ]\n },\n "predicate_type": "rea.javascript-runtime-reconciliation",\n "provider": {\n "id": "rea-javascript-runtime-reconciliation",\n "name": "REA JavaScript runtime reconciliation",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n },\n {\n "left": {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "kind": "retained-evidence"\n },\n "right": {\n "evidence_id": "ev_c2754cf75e286b839ed825f247062067a6472acb1e7b6ce0f9aaceec6448d6d7",\n "kind": "retained-evidence"\n }\n }\n ],\n "properties": {\n "left": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Left-hand input used for comparison or differential execution."\n },\n "left_native_observations": {\n "default": [],\n "description": "Value for left native observations.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "right": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Right-hand input used for comparison or differential execution."\n },\n "right_native_observations": {\n "default": [],\n "description": "Value for right native observations.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "left",\n "right"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "compare_application_versions",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "comparison_id": {\n "pattern": "^javc_[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "left_graph_omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_graph_status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "right_graph_omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_graph_status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial",\n "truncated"\n ],\n "type": "string"\n }\n },\n "required": [\n "left_graph_status",\n "right_graph_status",\n "left_graph_omitted_count",\n "right_graph_omitted_count",\n "status"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "graph": {\n "additionalProperties": false,\n "properties": {\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "edge_id": {\n "pattern": "^jag_edge_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "relationship-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n },\n "relation": {\n "enum": [\n "contains",\n "loads",\n "imports",\n "maps_to",\n "exposes",\n "sends",\n "invokes",\n "handles",\n "calls",\n "persists_to",\n "observed_as",\n "changed_from"\n ],\n "type": "string"\n },\n "source_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "target_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "source_node_id",\n "target_node_id",\n "relation",\n "properties",\n "evidence",\n "edge_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "identity": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "global-exact",\n "type": "string"\n },\n "strategy": {\n "const": "content-digest",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "original_source": {\n "type": "string"\n },\n "source_map_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "stability": {\n "const": "source-map-exact",\n "type": "string"\n },\n "strategy": {\n "const": "source-map-original",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "source_map_sha256",\n "original_source",\n "source_sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "canonical-path",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "minLength": 1,\n "type": "string"\n },\n "basis": {\n "items": {\n "enum": [\n "syntax-tree",\n "control-flow",\n "imports",\n "exports",\n "literals",\n "api-usage"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fingerprint_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "stability": {\n "const": "cross-version-inference",\n "type": "string"\n },\n "strategy": {\n "const": "structural-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "algorithm",\n "fingerprint_sha256",\n "basis"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "key": {\n "type": "string"\n },\n "namespace": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "artifact-version",\n "type": "string"\n },\n "strategy": {\n "const": "artifact-local-key",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "artifact_sha256",\n "namespace",\n "key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_key": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "capture-only",\n "type": "string"\n },\n "strategy": {\n "const": "runtime-instance",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "capture_sha256",\n "runtime_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "observation_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope": {\n "minLength": 1,\n "type": "string"\n },\n "stability": {\n "const": "observation-only",\n "type": "string"\n },\n "strategy": {\n "const": "observation-fingerprint",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability",\n "observation_sha256",\n "scope"\n ],\n "type": "object"\n }\n ]\n },\n "kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "observations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "available",\n "artifact_id",\n "sha256"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "authority": {\n "enum": [\n "artifact-bytes",\n "ast-static-analysis",\n "static-relationship-inference",\n "passive-cdp-runtime",\n "cross-layer-reconciliation",\n "cross-version-comparison",\n "controlled-replay",\n "native-analysis-provider",\n "managed-static-analysis",\n "historical-reference",\n "user-assertion",\n "unknown"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low",\n "unknown"\n ],\n "type": "string"\n },\n "coverage": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "const": 0,\n "type": "number"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "omitted_count": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "partial",\n "type": "string"\n },\n "truncated": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "limits": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "unit": {\n "enum": [\n "items",\n "bytes",\n "milliseconds",\n "depth",\n "characters",\n "instructions",\n "other"\n ],\n "type": "string"\n },\n "value": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "name",\n "value",\n "unit"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "omitted_count": {\n "type": "null"\n },\n "status": {\n "enum": [\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "truncated": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "status",\n "truncated",\n "omitted_count",\n "limits"\n ],\n "type": "object"\n }\n ]\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "extractor": {\n "additionalProperties": false,\n "properties": {\n "executable_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "operation",\n "executable_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "location": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "value": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "kind": {\n "const": "source-range",\n "type": "string"\n },\n "source": {\n "minLength": 1,\n "type": "string"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "source",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "url",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "frame_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "kind": {\n "const": "runtime",\n "type": "string"\n },\n "script_key": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "target_key": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "capture_sha256",\n "target_key",\n "frame_key",\n "script_key"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "native-address",\n "type": "string"\n },\n "symbol": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "address",\n "symbol"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "export_path": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "package-export",\n "type": "string"\n },\n "package_name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "package_name",\n "export_path"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "available",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "enum": [\n "not-applicable",\n "not-observed",\n "truncated",\n "provider-unavailable",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "available",\n "reason",\n "detail"\n ],\n "type": "object"\n }\n ]\n },\n "state": {\n "enum": [\n "observed",\n "inferred",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "authority",\n "state",\n "confidence",\n "artifact",\n "location",\n "extractor",\n "coverage",\n "limitations",\n "evidence_ids"\n ],\n "type": "object"\n },\n "identifier_strategy": {\n "additionalProperties": false,\n "properties": {\n "stability": {\n "const": "observation-exact",\n "type": "string"\n },\n "strategy": {\n "const": "semantic-content-sha256",\n "type": "string"\n }\n },\n "required": [\n "strategy",\n "stability"\n ],\n "type": "object"\n },\n "label": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "observation_id": {\n "pattern": "^jag_observation_[a-f0-9]{64}$",\n "type": "string"\n },\n "properties": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "minLength": 1,\n "type": "string"\n },\n "type": "object"\n }\n },\n "required": [\n "label",\n "properties",\n "evidence",\n "observation_id",\n "identifier_strategy"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "node_id",\n "kind",\n "identity",\n "observations"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "root_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "schema": {\n "const": "JavaScriptApplicationGraph",\n "type": "string"\n }\n },\n "required": [\n "schema",\n "root_node_ids",\n "nodes",\n "edges",\n "coverage",\n "limitations",\n "graph_id"\n ],\n "type": "object"\n },\n "items": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "content",\n "location",\n "properties",\n "relationships",\n "availability",\n "coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^javc_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "left_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "exact-node-identity",\n "exact-content-digest",\n "exact-module-source-digest"\n ],\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "exact",\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "source-map-identity",\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "high",\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "structural-fingerprint",\n "semantic-key"\n ],\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "medium",\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n }\n ]\n },\n "node_kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "right_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "changed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "node_kind",\n "dimensions",\n "evidence_links",\n "limitations",\n "status",\n "left_node_id",\n "right_node_id",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "content",\n "location",\n "properties",\n "relationships",\n "availability",\n "coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^javc_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "left_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n },\n "node_kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "right_node_id": {\n "type": "null"\n },\n "status": {\n "enum": [\n "removed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "node_kind",\n "dimensions",\n "evidence_links",\n "limitations",\n "status",\n "left_node_id",\n "right_node_id",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "content",\n "location",\n "properties",\n "relationships",\n "availability",\n "coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^javc_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "left_node_id": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "unmatched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n },\n "node_kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "right_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "added",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "node_kind",\n "dimensions",\n "evidence_links",\n "limitations",\n "status",\n "left_node_id",\n "right_node_id",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "content",\n "location",\n "properties",\n "relationships",\n "availability",\n "coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^javc_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "left_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "prefixItems": [\n {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n ],\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "ambiguous",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n },\n "node_kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "right_node_id": {\n "type": "null"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "node_kind",\n "dimensions",\n "evidence_links",\n "limitations",\n "status",\n "left_node_id",\n "right_node_id",\n "match"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimensions": {\n "items": {\n "enum": [\n "content",\n "location",\n "properties",\n "relationships",\n "availability",\n "coverage"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "item_id": {\n "pattern": "^javc_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "left_node_id": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "match": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "const": "none",\n "type": "string"\n },\n "candidate_left_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "prefixItems": [\n {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n ],\n "type": "array"\n },\n "candidate_right_node_ids": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "confidence": {\n "const": "unknown",\n "type": "string"\n },\n "status": {\n "const": "ambiguous",\n "type": "string"\n }\n },\n "required": [\n "status",\n "basis",\n "confidence",\n "candidate_left_node_ids",\n "candidate_right_node_ids"\n ],\n "type": "object"\n },\n "node_kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "right_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "item_id",\n "node_kind",\n "dimensions",\n "evidence_links",\n "limitations",\n "status",\n "left_node_id",\n "right_node_id",\n "match"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "left": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "root_artifact_sha256"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "matching": {\n "additionalProperties": false,\n "properties": {\n "ambiguous": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exact_content_digest": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exact_module_source_digest": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "exact_node_identity": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "semantic_key": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "source_map_identity": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "structural_fingerprint": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unmatched": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "exact_node_identity",\n "exact_content_digest",\n "exact_module_source_digest",\n "source_map_identity",\n "structural_fingerprint",\n "semantic_key",\n "ambiguous",\n "unmatched"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "root_artifact_sha256"\n ],\n "type": "object"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "changed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "unknown"\n ],\n "type": "object"\n }\n },\n "required": [\n "comparison_id",\n "left",\n "right",\n "summary",\n "matching",\n "items",\n "graph",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Compare Application Versions"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare a cryptographically committed HistoricalSourceGraph with authenticated JavaScript Application Graph Evidence supplied inline or by exact same-session retained reference. Uses explicit exact-digest, source-map path, current-path, suffix, and basename signals with stable weights. Classifies unchanged, modified, removed, split, merged, duplicated, and unknown; incomplete coverage and ambiguous weak signals never become absence or forced matches.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "application": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "format": "directory",\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [],\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "limitations": [],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "inventory_graph_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "inventory_manifest_id": "agm_2222222222222222222222222222222222222222222222222222222222222222",\n "limitations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "semantic_graph": {\n "application_graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "coverage": {\n "families": [\n {\n "family": "boundary",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "call-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "child-process",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "closure",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "configuration",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "data-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "event",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "object-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "promise-ownership",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "request",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "resource-lifecycle",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "timer",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n }\n ],\n "limits": [],\n "omitted_nodes": 0,\n "omitted_relations": 0,\n "status": "unknown",\n "truncated": false\n },\n "fingerprints": [],\n "graph_id": "jsrg_08c925d2bf4ef56d770e0e67901d5bdfb610744cf001a50d36c9bb6dcb195508",\n "limitations": [\n "Semantic relations are unavailable in this minimal example."\n ],\n "nodes": [\n {\n "application_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "function_node_id": null,\n "identifier_strategy": {\n "stability": "artifact-version",\n "strategy": "semantic-content-sha256"\n },\n "identity": {\n "artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "module_path": "renderer.js",\n "role_key": "example-module",\n "source_range": null\n },\n "kind": "module",\n "label": "renderer.js",\n "node_id": "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a",\n "properties": {}\n }\n ],\n "relations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "root_node_ids": [\n "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a"\n ],\n "schema": "JavaScriptSemanticRelationGraph",\n "unknowns": []\n },\n "statistics": {\n "findings": 0,\n "invalid_utf8_files": 0,\n "modules": 0,\n "nested_asar_containers": 0,\n "parse_failures": 0,\n "parsed_javascript_files": 1,\n "relevant_files": 1,\n "text_bytes_read": 29,\n "truncated_scopes": 0,\n "visited_ast_nodes": 1\n },\n "summary": {\n "browser_windows": 0,\n "context_bridge_apis": 0,\n "explicit_web_preferences": 0,\n "exposed_api_members": 0,\n "ipc": {\n "ambiguous_renderer_transmissions": 0,\n "dynamic_channel_operations": 0,\n "literal_channels": 0,\n "main_handlers": 0,\n "operations": 0,\n "paired_renderer_transmissions": 0,\n "renderer_listeners": 0,\n "renderer_transmissions": 0,\n "unpaired_literal_renderer_transmissions": 0\n },\n "native_addon_bindings": 0,\n "preload_entrypoints": 0,\n "resolved_native_addon_bindings": 0,\n "resolved_utility_entrypoints": 0,\n "sender_validation_observations": 0,\n "utility_processes": 0\n }\n },\n "operation": "analyze_javascript_application",\n "parameters": {},\n "predicate_type": "rea.javascript-application-analysis",\n "provider": {\n "id": "rea-javascript-application",\n "name": "REA JavaScript application analyzer",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "directory",\n "local_path": "/Applications/Example.app/Contents/Resources/app",\n "name": "app"\n }\n },\n "reference": {\n "authority": "historical-reference",\n "entries": [\n {\n "classifications": [\n "source"\n ],\n "kind": "directory",\n "limitations": [],\n "path": "src",\n "tree_state": "enumerated"\n },\n {\n "classifications": [\n "source"\n ],\n "content_state": "hashed",\n "kind": "file",\n "language": "TypeScript",\n "limitations": [],\n "path": "src/main.ts",\n "sha256": "2222222222222222222222222222222222222222222222222222222222222222",\n "size": 128\n }\n ],\n "exclusions": [],\n "inventory_state": "complete",\n "languages": [\n "TypeScript"\n ],\n "limitations": [],\n "manifests": [],\n "parse_failures": [],\n "provenance": {\n "caller": "contract-example",\n "importer": "rea",\n "importer_version": "1"\n },\n "relationships": [],\n "root_alias": "$REFERENCE_ROOT",\n "root_sha256": "683969414c3e674a368392213c78021322fb39609b0c57f346b22da6464b2110",\n "schema": "HistoricalSourceGraph",\n "vcs": {\n "dirty": null,\n "head": null,\n "kind": "none"\n }\n }\n }\n ],\n "properties": {\n "application": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Value for application."\n },\n "reference": {\n "additionalProperties": false,\n "description": "Value for reference.",\n "properties": {\n "authority": {\n "const": "historical-reference",\n "description": "Value for authority.",\n "type": "string"\n },\n "entries": {\n "description": "Value for entries.",\n "items": {\n "anyOf": [\n {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "classifications": {\n "description": "Value for classifications.",\n "items": {\n "enum": [\n "source",\n "test",\n "config",\n "manifest",\n "generated",\n "vendor",\n "documentation",\n "unknown"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "content_state": {\n "const": "hashed",\n "description": "Value for content state.",\n "type": "string"\n },\n "kind": {\n "const": "file",\n "description": "Value for kind.",\n "type": "string"\n },\n "language": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for language."\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "size": {\n "description": "Value for size.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "path",\n "classifications",\n "limitations",\n "kind",\n "language",\n "sha256",\n "size",\n "content_state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "classifications": {\n "description": "Value for classifications.",\n "items": {\n "enum": [\n "source",\n "test",\n "config",\n "manifest",\n "generated",\n "vendor",\n "documentation",\n "unknown"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "content_state": {\n "description": "Value for content state.",\n "enum": [\n "redacted-secret",\n "excluded",\n "unreadable",\n "unknown"\n ],\n "type": "string"\n },\n "kind": {\n "const": "file",\n "description": "Value for kind.",\n "type": "string"\n },\n "language": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for language."\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "description": "Value for sha256.",\n "type": "null"\n },\n "size": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for size."\n }\n },\n "required": [\n "path",\n "classifications",\n "limitations",\n "kind",\n "language",\n "sha256",\n "size",\n "content_state"\n ],\n "type": "object"\n }\n ]\n },\n {\n "additionalProperties": false,\n "properties": {\n "classifications": {\n "description": "Value for classifications.",\n "items": {\n "enum": [\n "source",\n "test",\n "config",\n "manifest",\n "generated",\n "vendor",\n "documentation",\n "unknown"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "kind": {\n "const": "directory",\n "description": "Value for kind.",\n "type": "string"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "tree_state": {\n "description": "Value for tree state.",\n "enum": [\n "enumerated",\n "partial",\n "excluded",\n "unreadable",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "classifications",\n "limitations",\n "kind",\n "tree_state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "classifications": {\n "description": "Value for classifications.",\n "items": {\n "enum": [\n "source",\n "test",\n "config",\n "manifest",\n "generated",\n "vendor",\n "documentation",\n "unknown"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "kind": {\n "const": "symlink",\n "description": "Value for kind.",\n "type": "string"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "target": {\n "description": "Value for target.",\n "minLength": 1,\n "type": "string"\n },\n "target_state": {\n "description": "Value for target state.",\n "enum": [\n "internal",\n "external",\n "missing",\n "unreadable",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "classifications",\n "limitations",\n "kind",\n "target",\n "target_state"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "exclusions": {\n "description": "Value for exclusions.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "description": "Value for reason.",\n "enum": [\n "configured-secret",\n "symlink-escape",\n "size-limit",\n "inventory-limit",\n "unreadable",\n "caller-excluded"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "inventory_state": {\n "description": "Value for inventory state.",\n "enum": [\n "complete",\n "partial",\n "unknown"\n ],\n "type": "string"\n },\n "languages": {\n "description": "Value for languages.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "manifests": {\n "description": "Value for manifests.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "parse_failures": {\n "description": "Value for parse failures.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "parser": {\n "description": "Value for parser.",\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "description": "Value for reason.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "parser",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "provenance": {\n "additionalProperties": false,\n "description": "Value for provenance.",\n "properties": {\n "caller": {\n "description": "Value for caller.",\n "minLength": 1,\n "pattern": "^[\\\\w .:@/+-]+$",\n "type": "string"\n },\n "importer": {\n "description": "Value for importer.",\n "minLength": 1,\n "type": "string"\n },\n "importer_version": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for importer version."\n }\n },\n "required": [\n "importer",\n "importer_version",\n "caller"\n ],\n "type": "object"\n },\n "relationships": {\n "description": "Value for relationships.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "from_path": {\n "description": "Local filesystem path for from.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "imports",\n "requires",\n "references",\n "declares-module"\n ],\n "type": "string"\n },\n "parse_state": {\n "description": "Value for parse state.",\n "enum": [\n "parsed",\n "partial",\n "unknown"\n ],\n "type": "string"\n },\n "resolution": {\n "description": "Value for resolution.",\n "enum": [\n "internal",\n "external",\n "unresolved",\n "unknown"\n ],\n "type": "string"\n },\n "to": {\n "description": "Value for to.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "from_path",\n "to",\n "kind",\n "resolution",\n "parse_state"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "root_alias": {\n "const": "$REFERENCE_ROOT",\n "description": "Value for root alias.",\n "type": "string"\n },\n "root_sha256": {\n "description": "Exact SHA-256 digest of root.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "schema": {\n "const": "HistoricalSourceGraph",\n "description": "Value for schema.",\n "type": "string"\n },\n "vcs": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "dirty": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for dirty."\n },\n "head": {\n "description": "Value for head.",\n "pattern": "^(?:[a-f0-9]{40}|[a-f0-9]{64})$",\n "type": "string"\n },\n "kind": {\n "const": "git",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "head",\n "dirty"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dirty": {\n "description": "Value for dirty.",\n "type": "null"\n },\n "head": {\n "description": "Value for head.",\n "type": "null"\n },\n "kind": {\n "const": "none",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "head",\n "dirty"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dirty": {\n "description": "Value for dirty.",\n "type": "null"\n },\n "head": {\n "description": "Value for head.",\n "type": "null"\n },\n "kind": {\n "const": "unknown",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "head",\n "dirty"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for vcs."\n }\n },\n "required": [\n "schema",\n "authority",\n "root_alias",\n "root_sha256",\n "inventory_state",\n "entries",\n "relationships",\n "parse_failures",\n "exclusions",\n "languages",\n "manifests",\n "vcs",\n "provenance",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "reference",\n "application"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "compare_source_to_bundle",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "application": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_artifact_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "root_artifact_sha256"\n ],\n "type": "object"\n },\n "comparison_id": {\n "pattern": "^stbc_[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "application_graph_status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "candidate_evaluations": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "reference_inventory_state": {\n "enum": [\n "complete",\n "partial",\n "unknown"\n ],\n "type": "string"\n },\n "retained_application_nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "retained_source_files": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "reference_inventory_state",\n "application_graph_status",\n "retained_source_files",\n "retained_application_nodes",\n "candidate_evaluations"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 1,\n "minItems": 1,\n "type": "array"\n },\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "low"\n ],\n "type": "string"\n },\n "current_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "current_node_kind": {\n "enum": [\n "package",\n "installer",\n "artifact",\n "asar-entry",\n "electron-main",\n "electron-preload",\n "electron-renderer",\n "electron-utility",\n "javascript-asset",\n "javascript-chunk",\n "javascript-module",\n "source-map",\n "source-module",\n "browser-window",\n "frame",\n "target",\n "context-bridge-api",\n "ipc-channel",\n "ipc-handler",\n "worker",\n "service-worker",\n "endpoint",\n "storage",\n "native-addon",\n "native-export",\n "managed-assembly",\n "managed-module",\n "managed-type",\n "managed-method",\n "managed-field",\n "managed-pinvoke-import",\n "managed-native-implementation",\n "runtime-script-instance",\n "unknown"\n ],\n "type": "string"\n },\n "score": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "signals": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "current_values": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "kind": {\n "enum": [\n "exact-source-digest",\n "source-map-original-path",\n "current-path-exact",\n "current-path-suffix",\n "basename-match",\n "language-extension"\n ],\n "type": "string"\n },\n "source_value": {\n "minLength": 1,\n "type": "string"\n },\n "weight": {\n "maximum": 100,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "weight",\n "source_value",\n "current_values"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "current_node_id",\n "current_node_kind",\n "score",\n "confidence",\n "signals"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "confidence": {\n "enum": [\n "exact",\n "high",\n "medium",\n "unknown"\n ],\n "type": "string"\n },\n "current_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "mapping_id": {\n "pattern": "^stbc_item_[a-f0-9]{64}$",\n "type": "string"\n },\n "source_language": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "source_path": {\n "minLength": 1,\n "type": "string"\n },\n "source_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "unchanged",\n "modified",\n "removed",\n "split",\n "merged",\n "duplicated",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "mapping_id",\n "source_path",\n "source_sha256",\n "source_language",\n "status",\n "confidence",\n "current_node_ids",\n "candidates",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "reference": {\n "additionalProperties": false,\n "properties": {\n "inventory_state": {\n "enum": [\n "complete",\n "partial",\n "unknown"\n ],\n "type": "string"\n },\n "root_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "root_sha256",\n "inventory_state"\n ],\n "type": "object"\n },\n "scoring": {\n "additionalProperties": false,\n "properties": {\n "algorithm": {\n "const": "rea-source-to-bundle-signals",\n "type": "string"\n },\n "minimum_candidate_score": {\n "const": 20,\n "type": "number"\n },\n "weights": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "signal": {\n "enum": [\n "exact-source-digest",\n "source-map-original-path",\n "current-path-exact",\n "current-path-suffix",\n "basename-match",\n "language-extension"\n ],\n "type": "string"\n },\n "weight": {\n "maximum": 100,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "signal",\n "weight"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "algorithm",\n "minimum_candidate_score",\n "weights"\n ],\n "type": "object"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "duplicated": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "merged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "modified": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "split": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unchanged",\n "modified",\n "removed",\n "split",\n "merged",\n "duplicated",\n "unknown"\n ],\n "type": "object"\n },\n "unmapped_current_node_ids": {\n "items": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "comparison_id",\n "reference",\n "application",\n "scoring",\n "summary",\n "items",\n "unmapped_current_node_ids",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Compare Source To Bundle"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare static return shapes for one exact module/export selector on each authenticated JavaScript Application Graph supplied as inline Evidence or exact same-session retained references. Variants pair only by reciprocal unique literal discriminants; dynamic values, incomplete properties, and ambiguous variants remain unknown. Reports JSON Pointer changes without executing JavaScript; runtime behavior requires a separate agent-run probe.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "format": "directory",\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [],\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "limitations": [],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "inventory_graph_sha256": "3333333333333333333333333333333333333333333333333333333333333333",\n "inventory_manifest_id": "agm_2222222222222222222222222222222222222222222222222222222222222222",\n "limitations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "semantic_graph": {\n "application_graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "coverage": {\n "families": [\n {\n "family": "boundary",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "call-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "child-process",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "closure",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "configuration",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "data-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "event",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "object-flow",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "promise-ownership",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "request",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "resource-lifecycle",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n },\n {\n "family": "timer",\n "omitted_relations": 0,\n "retained_relations": 0,\n "status": "unknown",\n "unknown_ids": []\n }\n ],\n "limits": [],\n "omitted_nodes": 0,\n "omitted_relations": 0,\n "status": "unknown",\n "truncated": false\n },\n "fingerprints": [],\n "graph_id": "jsrg_08c925d2bf4ef56d770e0e67901d5bdfb610744cf001a50d36c9bb6dcb195508",\n "limitations": [\n "Semantic relations are unavailable in this minimal example."\n ],\n "nodes": [\n {\n "application_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n ],\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "function_node_id": null,\n "identifier_strategy": {\n "stability": "artifact-version",\n "strategy": "semantic-content-sha256"\n },\n "identity": {\n "artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "module_path": "renderer.js",\n "role_key": "example-module",\n "source_range": null\n },\n "kind": "module",\n "label": "renderer.js",\n "node_id": "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a",\n "properties": {}\n }\n ],\n "relations": [],\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "root_node_ids": [\n "jsrg_node_9476602f1f74433a2bd3dadb040a25734d203b8f831b31a8d96f7efdfc5cd66a"\n ],\n "schema": "JavaScriptSemanticRelationGraph",\n "unknowns": []\n },\n "statistics": {\n "findings": 0,\n "invalid_utf8_files": 0,\n "modules": 0,\n "nested_asar_containers": 0,\n "parse_failures": 0,\n "parsed_javascript_files": 1,\n "relevant_files": 1,\n "text_bytes_read": 29,\n "truncated_scopes": 0,\n "visited_ast_nodes": 1\n },\n "summary": {\n "browser_windows": 0,\n "context_bridge_apis": 0,\n "explicit_web_preferences": 0,\n "exposed_api_members": 0,\n "ipc": {\n "ambiguous_renderer_transmissions": 0,\n "dynamic_channel_operations": 0,\n "literal_channels": 0,\n "main_handlers": 0,\n "operations": 0,\n "paired_renderer_transmissions": 0,\n "renderer_listeners": 0,\n "renderer_transmissions": 0,\n "unpaired_literal_renderer_transmissions": 0\n },\n "native_addon_bindings": 0,\n "preload_entrypoints": 0,\n "resolved_native_addon_bindings": 0,\n "resolved_utility_entrypoints": 0,\n "sender_validation_observations": 0,\n "utility_processes": 0\n }\n },\n "operation": "analyze_javascript_application",\n "parameters": {},\n "predicate_type": "rea.javascript-application-analysis",\n "provider": {\n "id": "rea-javascript-application",\n "name": "REA JavaScript application analyzer",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "directory",\n "local_path": "/Applications/Example.app/Contents/Resources/app",\n "name": "app"\n }\n },\n "left_export_name": "default",\n "left_module_path": "parser.mjs",\n "right": {\n "authority": "analyst-inference",\n "confidence": "inferred",\n "environment": null,\n "evidence_id": "ev_c2754cf75e286b839ed825f247062067a6472acb1e7b6ce0f9aaceec6448d6d7",\n "evidence_links": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54"\n ],\n "limitations": [\n "Artifact identity is unavailable for this observation.",\n "A static module resident in an observed bundle is not reported as executed.",\n "Caller-declared file and URL mappings are inference inputs and do not expand CDP origin or filesystem-root authority.",\n "Digest equality establishes byte identity but not code reachability, module initialization, or feature execution.",\n "Not-observed means absent from the bounded captures in scope, never globally unloaded.",\n "Only caller-supplied, semantically verified Evidence records were reconciled.",\n "Source-map authority remains separate from passive runtime authority."\n ],\n "locations": [],\n "normalized_result": {\n "coverage": {\n "omitted_graph_items": 0,\n "omitted_reconciliation_items": 0,\n "omitted_runtime_entities": 0,\n "omitted_static_load_states": 0,\n "status": "complete-within-inputs",\n "truncated": false\n },\n "evidence_links": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54"\n ],\n "graph": {\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "edges": [\n {\n "edge_id": "jag_edge_3799eb1c2b0f224013a0edba8911a8b9edabd5fa6da2f893a6cfa24a899c9463",\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "cross-layer-reconciliation",\n "confidence": "high",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-runtime-reconciliation",\n "operation": "reconcile_javascript_runtime",\n "version": "1"\n },\n "limitations": [\n "Equal content digests establish byte identity, not execution causality.",\n "Passive script parsing or target presence does not prove feature execution."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "target_key": "example-target"\n }\n },\n "state": "inferred"\n },\n "identifier_strategy": {\n "stability": "relationship-exact",\n "strategy": "semantic-content-sha256"\n },\n "properties": {\n "basis": "content-and-location",\n "reconciliation_id": "jrr_item_ea9d751c4bf3a2234af56b80d5ac8a1f953da9a9477cd46586a62934ff4a5fb2",\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753"\n },\n "relation": "observed_as",\n "source_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "target_node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e"\n },\n {\n "edge_id": "jag_edge_f3c06b02c5ac77056b5d2a9f8de834ea0a9123b01dc369a011e3adc4a535338b",\n "evidence": {\n "artifact": {\n "available": false,\n "detail": "The relationship is runtime metadata, not standalone bytes.",\n "reason": "not-applicable"\n },\n "authority": "passive-cdp-runtime",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-cdp-electron",\n "operation": "inspect_electron_page",\n "version": "1"\n },\n "limitations": [\n "The relationship was observed only within one bounded passive CDP capture."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "target_key": "example-target"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "relationship-exact",\n "strategy": "semantic-content-sha256"\n },\n "properties": {\n "basis": "passive-cdp-observation"\n },\n "relation": "loads",\n "source_node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "target_node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e"\n }\n ],\n "graph_id": "jag_0536a9584eb91a16f78f71e8bef4cffc3105df18098fe65a7b114add520203ca",\n "limitations": [\n "Cross-layer observed_as edges are inferences; digest equality proves byte identity only.",\n "Runtime presence is bounded to passive capture windows and does not prove feature execution.",\n "Source-map relationships retain their original static authority and are not promoted into runtime matches."\n ],\n "nodes": [\n {\n "identity": {\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "stability": "global-exact",\n "strategy": "content-digest"\n },\n "kind": "javascript-asset",\n "node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "artifact-bytes",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-javascript-application",\n "operation": "inventory-relevant-file",\n "version": "1"\n },\n "limitations": [],\n "location": {\n "available": true,\n "value": {\n "kind": "artifact-path",\n "path": "renderer.js"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "renderer.js",\n "observation_id": "jag_observation_20b4d076ce370db14ae2b477915624c62e0c4a0c9e56803a2c5cb4fa60c16230",\n "properties": {\n "bytes": 29,\n "path": "renderer.js"\n }\n }\n ]\n },\n {\n "identity": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "runtime_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "stability": "capture-only",\n "strategy": "runtime-instance"\n },\n "kind": "runtime-script-instance",\n "node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "artifact_id": "art_c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64",\n "available": true,\n "sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n },\n "authority": "passive-cdp-runtime",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-cdp-electron",\n "operation": "inspect_electron_page",\n "version": "1"\n },\n "limitations": [\n "The entity was observed only within one bounded passive CDP capture."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": "electron_script_4444444444444444444444444444444444444444444444444444444444444444",\n "target_key": "example-target"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "/Applications/Example.app/Contents/Resources/app/renderer.js",\n "observation_id": "jag_observation_9315403b875ea70c310909daf797780ab9045ff8f4803db38a9184e4f0704f11",\n "properties": {\n "cdp_hash": "example",\n "is_module": true,\n "language": "JavaScript",\n "length": 29,\n "location": "/Applications/Example.app/Contents/Resources/app/renderer.js",\n "source_sha256": "c4c0d760a4c84f0e182de99f6ef89ef10831f8170e486c4d4bd934924151ba64"\n }\n }\n ]\n },\n {\n "identity": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "runtime_key": "example-target",\n "stability": "capture-only",\n "strategy": "runtime-instance"\n },\n "kind": "target",\n "node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "observations": [\n {\n "evidence": {\n "artifact": {\n "available": false,\n "detail": "This runtime entity has no standalone artifact bytes.",\n "reason": "not-applicable"\n },\n "authority": "passive-cdp-runtime",\n "confidence": "exact",\n "coverage": {\n "limits": [],\n "omitted_count": 0,\n "status": "complete",\n "truncated": false\n },\n "evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "extractor": {\n "executable_sha256": null,\n "name": "rea-cdp-electron",\n "operation": "inspect_electron_page",\n "version": "1"\n },\n "limitations": [\n "The entity was observed only within one bounded passive CDP capture."\n ],\n "location": {\n "available": true,\n "value": {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "frame_key": null,\n "kind": "runtime",\n "script_key": null,\n "target_key": "example-target"\n }\n },\n "state": "observed"\n },\n "identifier_strategy": {\n "stability": "observation-exact",\n "strategy": "semantic-content-sha256"\n },\n "label": "Example",\n "observation_id": "jag_observation_844a03a29af96a0bcd4625e0377b67c439af68a462c5fa83009c7c27283c5dee",\n "properties": {\n "attached": false,\n "location": "/Applications/Example.app/Contents/Resources/app/index.html",\n "runtime_type": "page"\n }\n }\n ]\n }\n ],\n "root_node_ids": [\n "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265"\n ],\n "schema": "JavaScriptApplicationGraph"\n },\n "limitations": [\n "A static module resident in an observed bundle is not reported as executed.",\n "Caller-declared file and URL mappings are inference inputs and do not expand CDP origin or filesystem-root authority.",\n "Digest equality establishes byte identity but not code reachability, module initialization, or feature execution.",\n "Not-observed means absent from the bounded captures in scope, never globally unloaded.",\n "Only caller-supplied, semantically verified Evidence records were reconciled.",\n "Source-map authority remains separate from passive runtime authority."\n ],\n "reconciliation_id": "jrr_ec50425a5a5a5cb85aa40f13f72dcba54a8ab16170d9421a0277fbdf0fa4c9eb",\n "reconciliations": [\n {\n "basis": "none",\n "candidate_static_count": 0,\n "candidate_static_nodes": [],\n "confidence": "unknown",\n "entity_kind": "target",\n "reason": "no-static-content-match",\n "reconciliation_id": "jrr_item_abd5635b06340f082ef46bff8802a361c7b30011085f1c9e7437b29b451c1e5e",\n "runtime_evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "runtime_node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "static_layer_id": null,\n "static_node_id": null,\n "status": "unmatched"\n },\n {\n "basis": "content-and-location",\n "candidate_static_count": 1,\n "candidate_static_nodes": [\n {\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "static_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e"\n }\n ],\n "confidence": "high",\n "entity_kind": "script",\n "reason": "content-and-location-match",\n "reconciliation_id": "jrr_item_ea9d751c4bf3a2234af56b80d5ac8a1f953da9a9477cd46586a62934ff4a5fb2",\n "runtime_evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "runtime_node_id": "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e",\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "static_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "status": "matched"\n }\n ],\n "runtime_captures": [\n {\n "capture_sha256": "a4dd260f01b45e3df92482755baed7a1922b4af2226edc7f896bdbc4d2bbbd9c",\n "evidence_id": "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e",\n "frames": 0,\n "kind": "electron",\n "scripts": 1,\n "scripts_complete_within_scope": true,\n "target_key": "example-target",\n "target_location": "/Applications/Example.app/Contents/Resources/app/index.html",\n "target_node_id": "jag_node_f360c35c86d4893fcf83ef0fdef92cebd688fba883aa39698a1c12dd5a237265",\n "workers": 0\n }\n ],\n "source_map_authority": {\n "limitation": "Source-map declarations and original-source content retain separate static authority and are not used as primary runtime byte matches.",\n "runtime_script_declarations": 0,\n "static_layer_count": 1,\n "used_for_primary_matching": false\n },\n "static_layers": [\n {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "format": "directory",\n "graph_id": "jag_ab15669664efc0a86bb49c9c60241913cce6980dc2293fbeef6d9c862ca01d9d",\n "input_path": "/Applications/Example.app/Contents/Resources/app",\n "layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "role": "application",\n "root_artifact_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "runtime_mappings": []\n }\n ],\n "static_load_states": [\n {\n "kind": "javascript-asset",\n "reason": "runtime-script-correspondence",\n "runtime_node_ids": [\n "jag_node_af8ea5d1909955f39da1d844b2cad8f2f68c34b7d575ded6d04c00fa4095a88e"\n ],\n "static_layer_id": "jrl_96fcfc590e7ca88f94b602638421674d8b13ccc7d25efff15a15b03c33b34753",\n "static_node_id": "jag_node_3c82da5492d90eb5b0bee41c1a994d88b7ba98c0b4955270d70e57a76e224c2e",\n "status": "loaded"\n }\n ],\n "summary": {\n "ambiguous": 0,\n "matched": 1,\n "runtime_frames": 0,\n "runtime_scripts": 1,\n "runtime_targets": 1,\n "runtime_workers": 0,\n "static_loaded": 1,\n "static_not_observed": 0,\n "static_resident": 0,\n "static_unknown": 0,\n "unknown": 0,\n "unmatched": 1\n }\n },\n "operation": "reconcile_javascript_runtime",\n "parameters": {\n "runtime_evidence_ids": [\n "ev_a42606ee799a093832ad3d6cfd0edd50dc9b99f78cc6d5d9130b734d48a56b4e"\n ],\n "static_layers": [\n {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "role": "application",\n "runtime_mappings": []\n }\n ]\n },\n "predicate_type": "rea.javascript-runtime-reconciliation",\n "provider": {\n "id": "rea-javascript-runtime-reconciliation",\n "name": "REA JavaScript runtime reconciliation",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n },\n "right_export_name": "default",\n "right_module_path": "parser.mjs"\n },\n {\n "left": {\n "evidence_id": "ev_a4ae2c7b5c501fb6dff695b168f35a6dd157fa957410910684f3a4ce4c8b5d54",\n "kind": "retained-evidence"\n },\n "left_export_name": "default",\n "left_module_path": "parser.mjs",\n "right": {\n "evidence_id": "ev_c2754cf75e286b839ed825f247062067a6472acb1e7b6ce0f9aaceec6448d6d7",\n "kind": "retained-evidence"\n },\n "right_export_name": "default",\n "right_module_path": "parser.mjs"\n }\n ],\n "properties": {\n "left": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Left-hand input used for comparison or differential execution."\n },\n "left_export_name": {\n "description": "Value for left export name.",\n "minLength": 1,\n "type": "string"\n },\n "left_module_path": {\n "description": "Local filesystem path for left module.",\n "minLength": 1,\n "type": "string"\n },\n "right": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "const": "retained-evidence",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id"\n ],\n "type": "object"\n }\n ],\n "description": "Right-hand input used for comparison or differential execution."\n },\n "right_export_name": {\n "description": "Value for right export name.",\n "minLength": 1,\n "type": "string"\n },\n "right_module_path": {\n "description": "Local filesystem path for right module.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "left_module_path",\n "left_export_name",\n "right_module_path",\n "right_export_name",\n "left",\n "right"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "compare_javascript_export_shapes",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "change_id": {\n "pattern": "^jesc_change_[a-f0-9]{64}$",\n "type": "string"\n },\n "discriminant": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "path": {\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "value"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "left": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "absent",\n "type": "string"\n }\n },\n "required": [\n "availability"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "literal",\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "availability",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "union",\n "type": "string"\n },\n "values": {\n "items": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "availability",\n "values"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "unknown",\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "availability",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "left_source_range": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "path": {\n "type": "string"\n },\n "right": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "absent",\n "type": "string"\n }\n },\n "required": [\n "availability"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "literal",\n "type": "string"\n },\n "value": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "availability",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "union",\n "type": "string"\n },\n "values": {\n "items": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "availability",\n "values"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "const": "unknown",\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "availability",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "right_source_range": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "added",\n "removed",\n "changed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "change_id",\n "status",\n "path",\n "discriminant",\n "left",\n "right",\n "left_source_range",\n "right_source_range",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "comparison_id": {\n "pattern": "^jesc_[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "left_graph_status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "left_omitted_fields": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "left_omitted_property_coverage": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "left_source_omitted_variants": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "omitted_changes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "omitted_left_variants": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "omitted_right_variants": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "paired_variants": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "right_graph_status": {\n "enum": [\n "complete",\n "partial",\n "unknown",\n "unavailable"\n ],\n "type": "string"\n },\n "right_omitted_fields": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "right_omitted_property_coverage": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "right_source_omitted_variants": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "enum": [\n "complete-within-inputs",\n "partial",\n "truncated"\n ],\n "type": "string"\n },\n "unpaired_left_variants": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unpaired_right_variants": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "status",\n "left_graph_status",\n "right_graph_status",\n "paired_variants",\n "unpaired_left_variants",\n "unpaired_right_variants",\n "omitted_left_variants",\n "omitted_right_variants",\n "left_source_omitted_variants",\n "right_source_omitted_variants",\n "left_omitted_fields",\n "right_omitted_fields",\n "left_omitted_property_coverage",\n "right_omitted_property_coverage",\n "omitted_candidates",\n "omitted_changes"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "maxItems": 2,\n "minItems": 2,\n "type": "array"\n },\n "left": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "export_name": {\n "minLength": 1,\n "type": "string"\n },\n "matches_requested_export": {\n "type": "boolean"\n },\n "matches_requested_module": {\n "type": "boolean"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "module_path",\n "export_name",\n "matches_requested_module",\n "matches_requested_export"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested_export_name": {\n "minLength": 1,\n "type": "string"\n },\n "requested_module_path": {\n "minLength": 1,\n "type": "string"\n },\n "selected_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "selected",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "requested_module_path",\n "requested_export_name",\n "candidates",\n "omitted_candidates",\n "status",\n "selected_node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "export_name": {\n "minLength": 1,\n "type": "string"\n },\n "matches_requested_export": {\n "type": "boolean"\n },\n "matches_requested_module": {\n "type": "boolean"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "module_path",\n "export_name",\n "matches_requested_module",\n "matches_requested_export"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested_export_name": {\n "minLength": 1,\n "type": "string"\n },\n "requested_module_path": {\n "minLength": 1,\n "type": "string"\n },\n "selected_node_id": {\n "type": "null"\n },\n "status": {\n "enum": [\n "missing",\n "ambiguous"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "requested_module_path",\n "requested_export_name",\n "candidates",\n "omitted_candidates",\n "status",\n "selected_node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "export_name": {\n "minLength": 1,\n "type": "string"\n },\n "matches_requested_export": {\n "type": "boolean"\n },\n "matches_requested_module": {\n "type": "boolean"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "module_path",\n "export_name",\n "matches_requested_module",\n "matches_requested_export"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested_export_name": {\n "minLength": 1,\n "type": "string"\n },\n "requested_module_path": {\n "minLength": 1,\n "type": "string"\n },\n "selected_node_id": {\n "anyOf": [\n {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "requested_module_path",\n "requested_export_name",\n "candidates",\n "omitted_candidates",\n "status",\n "selected_node_id"\n ],\n "type": "object"\n }\n ]\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "right": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "export_name": {\n "minLength": 1,\n "type": "string"\n },\n "matches_requested_export": {\n "type": "boolean"\n },\n "matches_requested_module": {\n "type": "boolean"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "module_path",\n "export_name",\n "matches_requested_module",\n "matches_requested_export"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested_export_name": {\n "minLength": 1,\n "type": "string"\n },\n "requested_module_path": {\n "minLength": 1,\n "type": "string"\n },\n "selected_node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "selected",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "requested_module_path",\n "requested_export_name",\n "candidates",\n "omitted_candidates",\n "status",\n "selected_node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "export_name": {\n "minLength": 1,\n "type": "string"\n },\n "matches_requested_export": {\n "type": "boolean"\n },\n "matches_requested_module": {\n "type": "boolean"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "module_path",\n "export_name",\n "matches_requested_module",\n "matches_requested_export"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested_export_name": {\n "minLength": 1,\n "type": "string"\n },\n "requested_module_path": {\n "minLength": 1,\n "type": "string"\n },\n "selected_node_id": {\n "type": "null"\n },\n "status": {\n "enum": [\n "missing",\n "ambiguous"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "requested_module_path",\n "requested_export_name",\n "candidates",\n "omitted_candidates",\n "status",\n "selected_node_id"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "export_name": {\n "minLength": 1,\n "type": "string"\n },\n "matches_requested_export": {\n "type": "boolean"\n },\n "matches_requested_module": {\n "type": "boolean"\n },\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "node_id": {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "node_id",\n "module_path",\n "export_name",\n "matches_requested_module",\n "matches_requested_export"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "graph_id": {\n "pattern": "^jag_[a-f0-9]{64}$",\n "type": "string"\n },\n "omitted_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "requested_export_name": {\n "minLength": 1,\n "type": "string"\n },\n "requested_module_path": {\n "minLength": 1,\n "type": "string"\n },\n "selected_node_id": {\n "anyOf": [\n {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "graph_id",\n "requested_module_path",\n "requested_export_name",\n "candidates",\n "omitted_candidates",\n "status",\n "selected_node_id"\n ],\n "type": "object"\n }\n ]\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "changed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "added",\n "removed",\n "changed",\n "unknown"\n ],\n "type": "object"\n }\n },\n "required": [\n "comparison_id",\n "left",\n "right",\n "summary",\n "changes",\n "coverage",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Compare Javascript Export Shapes"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Generate the complete deterministic ReconstructionObligationLedger from an authenticated Evidence bundle, reviewed obligations, and an explicit reconstruction manifest. Static candidates remain candidates; duplicate ownership, missing original or reconstruction cases, missing parser/type, weak verifier authority, unenumerated claims, contradictions, dependencies, and residual unknowns fail closed. The inline result carries the full-ledger closure digest and typed per-obligation diagnostics.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "evidence_bundle": {\n "artifacts": [],\n "captures": [],\n "environments": [],\n "providers": [],\n "records": [],\n "scenarios": [],\n "unknowns": []\n },\n "manifest": {\n "bindings": [],\n "contradictions": []\n },\n "reviewed_obligations": []\n }\n ],\n "properties": {\n "evidence_bundle": {\n "description": "Value for evidence bundle.",\n "properties": {\n "artifacts": {\n "description": "Value for artifacts.",\n "items": {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n }\n },\n "required": [\n "digest",\n "format",\n "architecture"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "captures": {\n "description": "Value for captures.",\n "items": {\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "predicate_type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "environments": {\n "description": "Value for environments.",\n "items": {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "providers": {\n "description": "Value for providers.",\n "items": {\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "records": {\n "description": "Value for records.",\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links",\n "analysis_profile"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "scenarios": {\n "description": "Value for scenarios.",\n "items": {\n "properties": {\n "authority": {\n "const": "controlled-replay",\n "description": "Value for authority.",\n "type": "string"\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "authority"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "unknowns": {\n "description": "Value for unknowns.",\n "items": {\n "properties": {\n "contradicting_evidence_ids": {\n "description": "Ordered Evidence identifiers for the contradicting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "description": "Value for domain.",\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "description": "Ordered Evidence identifiers for the mutation observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for previous revision digest."\n },\n "question": {\n "description": "Concrete unresolved question to retain for later investigation.",\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "description": "Value for recommended probes.",\n "items": {\n "properties": {\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "description": "Value for relationships.",\n "items": {\n "properties": {\n "type": {\n "description": "Value for type.",\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required authority."\n },\n "required_confidence": {\n "description": "Value for required confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for id."\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for isolation."\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for platform."\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required environment."\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "properties": {\n "disposition": {\n "const": "verified",\n "description": "Value for disposition.",\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "properties": {\n "disposition": {\n "description": "Value for disposition.",\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for resolution."\n },\n "revision": {\n "description": "Value for revision.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "description": "Value for revision digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for scope digest."\n },\n "severity": {\n "description": "Value for severity.",\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "description": "Ordered Evidence identifiers for the supporting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "artifacts",\n "providers",\n "environments",\n "scenarios",\n "captures",\n "unknowns",\n "records"\n ],\n "type": "object"\n },\n "manifest": {\n "additionalProperties": false,\n "description": "Value for manifest.",\n "properties": {\n "bindings": {\n "description": "Value for bindings.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "fixtures": {\n "description": "Value for fixtures.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "case_kind": {\n "description": "Value for case kind.",\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fixture_id": {\n "description": "Value for fixture id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n }\n },\n "required": [\n "fixture_id",\n "case_kind",\n "authority",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "obligation_id": {\n "description": "Value for obligation id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "original_cases": {\n "description": "Value for original cases.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "location": {\n "description": "Value for location.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "owner": {\n "additionalProperties": false,\n "description": "Value for owner.",\n "properties": {\n "module_path": {\n "description": "Local filesystem path for module.",\n "minLength": 1,\n "type": "string"\n },\n "owner_sha256": {\n "description": "Exact SHA-256 digest of owner.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "symbol": {\n "description": "Value for symbol.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "module_path",\n "symbol",\n "owner_sha256"\n ],\n "type": "object"\n },\n "parser_type": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain_type": {\n "description": "Value for domain type.",\n "minLength": 1,\n "type": "string"\n },\n "parser": {\n "description": "Value for parser.",\n "minLength": 1,\n "type": "string"\n },\n "schema": {\n "description": "Value for schema.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "parser",\n "schema",\n "domain_type"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for parser type."\n },\n "verifier": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "claim_id": {\n "description": "Value for claim id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "command": {\n "description": "Value for command.",\n "minLength": 1,\n "type": "string"\n },\n "enumerated_obligation_ids": {\n "description": "Value for enumerated obligation ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "nondeterminism": {\n "additionalProperties": false,\n "description": "Value for nondeterminism.",\n "properties": {\n "mode": {\n "description": "Operation mode that selects the requested behavior.",\n "enum": [\n "total-order",\n "partial-order",\n "finite-traces"\n ],\n "type": "string"\n },\n "specification": {\n "description": "Value for specification.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mode",\n "specification"\n ],\n "type": "object"\n },\n "result_evidence_id": {\n "description": "Exact Evidence identifier for the result observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "pass",\n "fail",\n "unknown",\n "unsupported",\n "skipped"\n ],\n "type": "string"\n },\n "verifier_id": {\n "description": "Value for verifier id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n }\n },\n "required": [\n "verifier_id",\n "claim_id",\n "command",\n "authority",\n "status",\n "result_evidence_id",\n "enumerated_obligation_ids",\n "nondeterminism"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for verifier."\n }\n },\n "required": [\n "obligation_id",\n "owner",\n "parser_type",\n "original_cases",\n "fixtures",\n "verifier"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "contradictions": {\n "description": "Value for contradictions.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "obligation_id": {\n "description": "Value for obligation id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n }\n },\n "required": [\n "obligation_id",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "bindings",\n "contradictions"\n ],\n "type": "object"\n },\n "reviewed_obligations": {\n "description": "Value for reviewed obligations.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "application_layer": {\n "description": "Value for application layer.",\n "enum": [\n "cli",\n "protocol",\n "electron",\n "persistence",\n "process",\n "runtime",\n "packaging",\n "native-abi",\n "application",\n "other"\n ],\n "type": "string"\n },\n "dependency_obligation_ids": {\n "description": "Value for dependency obligation ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "disposition": {\n "description": "Value for disposition.",\n "enum": [\n "active",\n "blocked",\n "out-of-scope"\n ],\n "type": "string"\n },\n "family": {\n "description": "Value for family.",\n "minLength": 1,\n "type": "string"\n },\n "obligation_id": {\n "description": "Value for obligation id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "obligation_version": {\n "description": "Value for obligation version.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "required": {\n "description": "Value for required.",\n "type": "boolean"\n },\n "required_case_kinds": {\n "description": "Value for required case kinds.",\n "items": {\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "required_fixture_authority": {\n "description": "Value for required fixture authority.",\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "required_next_evidence": {\n "description": "Value for required next evidence.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "required_original_authority": {\n "description": "Value for required original authority.",\n "enum": [\n "static",\n "runtime",\n "process",\n "external"\n ],\n "type": "string"\n },\n "required_verifier_authority": {\n "description": "Value for required verifier authority.",\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "requires_parser_type": {\n "description": "Value for requires parser type.",\n "type": "boolean"\n },\n "residual_unknown_ids": {\n "description": "Value for residual unknown ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "review_evidence_ids": {\n "description": "Ordered Evidence identifiers for the review observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "target": {\n "additionalProperties": false,\n "description": "Value for target.",\n "properties": {\n "application_node_id": {\n "anyOf": [\n {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for application node id."\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Exact SHA-256 digest of artifact."\n },\n "location": {\n "description": "Value for location.",\n "minLength": 1,\n "type": "string"\n },\n "semantic_node_id": {\n "anyOf": [\n {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for semantic node id."\n }\n },\n "required": [\n "artifact_sha256",\n "application_node_id",\n "semantic_node_id",\n "location"\n ],\n "type": "object"\n },\n "title": {\n "description": "Value for title.",\n "minLength": 1,\n "type": "string"\n },\n "unavailable_authority": {\n "description": "Value for unavailable authority.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "obligation_id",\n "obligation_version",\n "title",\n "application_layer",\n "family",\n "target",\n "required",\n "required_case_kinds",\n "required_original_authority",\n "required_fixture_authority",\n "required_verifier_authority",\n "requires_parser_type",\n "dependency_obligation_ids",\n "residual_unknown_ids",\n "unavailable_authority",\n "required_next_evidence",\n "disposition",\n "review_evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "evidence_bundle",\n "reviewed_obligations",\n "manifest"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "build_reconstruction_obligation_ledger",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "closure_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "status": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n "dependency_graph": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "depends_on_obligation_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "obligation_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n }\n },\n "required": [\n "obligation_id",\n "depends_on_obligation_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "ledger_id": {\n "pattern": "^rol_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "obligations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "application_layer": {\n "enum": [\n "cli",\n "protocol",\n "electron",\n "persistence",\n "process",\n "runtime",\n "packaging",\n "native-abi",\n "application",\n "other"\n ],\n "type": "string"\n },\n "authority_references": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "location": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "enum": [\n "candidate",\n "observed",\n "reviewed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "authority",\n "state",\n "location"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "binding": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "fixtures": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "case_kind": {\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "fixture_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n }\n },\n "required": [\n "fixture_id",\n "case_kind",\n "authority",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "obligation_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "original_cases": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "location": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "owner": {\n "additionalProperties": false,\n "properties": {\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "owner_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "symbol": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "module_path",\n "symbol",\n "owner_sha256"\n ],\n "type": "object"\n },\n "parser_type": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "domain_type": {\n "minLength": 1,\n "type": "string"\n },\n "parser": {\n "minLength": 1,\n "type": "string"\n },\n "schema": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "parser",\n "schema",\n "domain_type"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "verifier": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "claim_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "command": {\n "minLength": 1,\n "type": "string"\n },\n "enumerated_obligation_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "nondeterminism": {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "enum": [\n "total-order",\n "partial-order",\n "finite-traces"\n ],\n "type": "string"\n },\n "specification": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "mode",\n "specification"\n ],\n "type": "object"\n },\n "result_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "pass",\n "fail",\n "unknown",\n "unsupported",\n "skipped"\n ],\n "type": "string"\n },\n "verifier_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n }\n },\n "required": [\n "verifier_id",\n "claim_id",\n "command",\n "authority",\n "status",\n "result_evidence_id",\n "enumerated_obligation_ids",\n "nondeterminism"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "obligation_id",\n "owner",\n "parser_type",\n "original_cases",\n "fixtures",\n "verifier"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "dependency_obligation_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "diagnostics": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "duplicate-definition",\n "missing-owner",\n "ambiguous-owner",\n "missing-parser-type",\n "missing-original-case",\n "weak-original-case-authority",\n "missing-case",\n "weak-fixture-authority",\n "missing-verifier",\n "verifier-does-not-enumerate",\n "missing-verifier-result",\n "weak-verifier-authority",\n "verifier-failed",\n "static-only",\n "contradiction",\n "contradiction-evidence-missing",\n "residual-unknown",\n "dependency-missing",\n "dependency-open",\n "unavailable-authority",\n "blocked",\n "out-of-scope"\n ],\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "code",\n "detail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "family": {\n "minLength": 1,\n "type": "string"\n },\n "obligation_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "obligation_version": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "observed_cases": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "kind": {\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "location": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "evidence_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "origin": {\n "enum": [\n "generated",\n "reviewed"\n ],\n "type": "string"\n },\n "required": {\n "type": "boolean"\n },\n "required_case_kinds": {\n "items": {\n "enum": [\n "positive",\n "negative",\n "malformed",\n "cancellation",\n "teardown"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "required_fixture_authority": {\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "required_next_evidence": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "required_original_authority": {\n "enum": [\n "static",\n "runtime",\n "process",\n "external"\n ],\n "type": "string"\n },\n "required_verifier_authority": {\n "enum": [\n "unit",\n "integration",\n "protocol",\n "renderer",\n "packaged-process",\n "native-abi",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "requires_parser_type": {\n "type": "boolean"\n },\n "residual_unknown_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "source_state": {\n "enum": [\n "candidate",\n "observed",\n "reviewed",\n "unknown"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "unowned",\n "characterized",\n "implemented",\n "verified",\n "contradicted",\n "blocked",\n "out-of-scope",\n "unknown"\n ],\n "type": "string"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "application_node_id": {\n "anyOf": [\n {\n "pattern": "^jag_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "artifact_sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "location": {\n "minLength": 1,\n "type": "string"\n },\n "semantic_node_id": {\n "anyOf": [\n {\n "pattern": "^jsrg_node_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "artifact_sha256",\n "application_node_id",\n "semantic_node_id",\n "location"\n ],\n "type": "object"\n },\n "title": {\n "minLength": 1,\n "type": "string"\n },\n "unavailable_authority": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "obligation_id",\n "obligation_version",\n "title",\n "origin",\n "application_layer",\n "family",\n "target",\n "authority_references",\n "source_state",\n "observed_cases",\n "required",\n "required_case_kinds",\n "required_original_authority",\n "required_fixture_authority",\n "required_verifier_authority",\n "requires_parser_type",\n "dependency_obligation_ids",\n "residual_unknown_ids",\n "unavailable_authority",\n "required_next_evidence",\n "binding",\n "status",\n "diagnostics"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "ownership_graph": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "module_path": {\n "minLength": 1,\n "type": "string"\n },\n "obligation_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "symbol": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "obligation_id",\n "module_path",\n "symbol"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reports": {\n "additionalProperties": false,\n "properties": {\n "contradicted_obligation_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "missing_owner_obligation_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "missing_verifier_obligation_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "residual_unknown_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]*$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "missing_owner_obligation_ids",\n "missing_verifier_obligation_ids",\n "contradicted_obligation_ids",\n "residual_unknown_ids"\n ],\n "type": "object"\n },\n "schema": {\n "const": "ReconstructionObligationLedger",\n "type": "string"\n },\n "status": {\n "enum": [\n "ready",\n "open",\n "failed",\n "unknown"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "by_application_layer": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "key": {\n "enum": [\n "cli",\n "protocol",\n "electron",\n "persistence",\n "process",\n "runtime",\n "packaging",\n "native-abi",\n "application",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "key",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "by_evidence_authority": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "key": {\n "enum": [\n "candidate",\n "observed",\n "reviewed",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "key",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "by_status": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "key": {\n "enum": [\n "unowned",\n "characterized",\n "implemented",\n "verified",\n "contradicted",\n "blocked",\n "out-of-scope",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "key",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "required_open": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "verified": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "required",\n "verified",\n "required_open",\n "by_status",\n "by_application_layer",\n "by_evidence_authority"\n ],\n "type": "object"\n }\n },\n "required": [\n "schema",\n "ledger_id",\n "closure_digest",\n "status",\n "coverage",\n "summary",\n "reports",\n "ownership_graph",\n "dependency_graph",\n "obligations",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Build Reconstruction Obligation Ledger"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": true\n },\n "description": "Evaluate inline evidence-backed reconstruction coverage against one named boundary. Missing ownership or inventory is partial; stale, weak, truncated, skipped, or unresolved proof is unknown; contradictions, failed proof, missing owners, and authority routing fail closed.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": false,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "boundary_id": "replacement.cli",\n "coverage": {\n "artifacts": [],\n "boundaries": [\n {\n "allowed_dispositions": [],\n "allowed_unknown_ids": [],\n "boundary_id": "replacement.cli",\n "required_claim_ids": [\n "claim.cli.help"\n ],\n "required_package_proof_kinds": [\n "clean-install",\n "authority-independence"\n ],\n "required_surface_ids": [\n "cli.help"\n ],\n "title": "CLI replacement"\n }\n ],\n "claims": [],\n "contradictions": [],\n "evidence_bundle": {\n "artifacts": [],\n "captures": [],\n "environments": [],\n "providers": [],\n "records": [],\n "scenarios": [],\n "unknowns": []\n },\n "owners": [],\n "package_proofs": [],\n "residual_unknown_ids": [],\n "surfaces": [],\n "verifier_contracts": [],\n "verifier_results": []\n }\n }\n ],\n "properties": {\n "boundary_id": {\n "description": "Exact reconstruction boundary identifier to evaluate.",\n "minLength": 1,\n "type": "string"\n },\n "coverage": {\n "additionalProperties": false,\n "description": "Exact reconstruction-coverage commitment to verify.",\n "properties": {\n "artifacts": {\n "description": "Value for artifacts.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "description": "Value for artifact id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "artifact_sha256": {\n "description": "Exact SHA-256 digest of artifact.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "environment_sha256": {\n "description": "Exact SHA-256 digest of environment.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "artifact_id",\n "artifact_sha256",\n "version",\n "environment_sha256",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "boundaries": {\n "description": "Value for boundaries.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "allowed_dispositions": {\n "description": "Value for allowed dispositions.",\n "items": {\n "enum": [\n "external",\n "non-goal"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "allowed_unknown_ids": {\n "description": "Value for allowed unknown ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "type": "array"\n },\n "boundary_id": {\n "description": "Exact reconstruction boundary identifier to evaluate.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "required_claim_ids": {\n "description": "Value for required claim ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "required_package_proof_kinds": {\n "description": "Value for required package proof kinds.",\n "items": {\n "enum": [\n "build",\n "clean-install",\n "package-contents",\n "runtime-routing",\n "authority-independence"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "required_surface_ids": {\n "description": "Value for required surface ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "title": {\n "description": "Value for title.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "boundary_id",\n "title",\n "required_surface_ids",\n "required_claim_ids",\n "required_package_proof_kinds",\n "allowed_dispositions",\n "allowed_unknown_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "claims": {\n "description": "Value for claims.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "claim_id": {\n "description": "Value for claim id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "kind": {\n "description": "Value for kind.",\n "minLength": 1,\n "type": "string"\n },\n "required_authority": {\n "description": "Value for required authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "required_dimensions": {\n "description": "Value for required dimensions.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "surface_ids": {\n "description": "Value for surface ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "title": {\n "description": "Value for title.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "claim_id",\n "title",\n "kind",\n "surface_ids",\n "required_dimensions",\n "required_authority"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "contradictions": {\n "description": "Value for contradictions.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "claim_ids": {\n "description": "Value for claim ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "type": "array"\n },\n "contradiction_id": {\n "description": "Value for contradiction id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "active",\n "resolved"\n ],\n "type": "string"\n },\n "surface_ids": {\n "description": "Value for surface ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "contradiction_id",\n "status",\n "surface_ids",\n "claim_ids",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_bundle": {\n "description": "Value for evidence bundle.",\n "properties": {\n "artifacts": {\n "description": "Value for artifacts.",\n "items": {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n }\n },\n "required": [\n "digest",\n "format",\n "architecture"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "captures": {\n "description": "Value for captures.",\n "items": {\n "properties": {\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "predicate_type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "environments": {\n "description": "Value for environments.",\n "items": {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "providers": {\n "description": "Value for providers.",\n "items": {\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "records": {\n "description": "Value for records.",\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links",\n "analysis_profile"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "scenarios": {\n "description": "Value for scenarios.",\n "items": {\n "properties": {\n "authority": {\n "const": "controlled-replay",\n "description": "Value for authority.",\n "type": "string"\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "authority"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "unknowns": {\n "description": "Value for unknowns.",\n "items": {\n "properties": {\n "contradicting_evidence_ids": {\n "description": "Ordered Evidence identifiers for the contradicting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "description": "Value for domain.",\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "description": "Ordered Evidence identifiers for the mutation observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for previous revision digest."\n },\n "question": {\n "description": "Concrete unresolved question to retain for later investigation.",\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "description": "Value for recommended probes.",\n "items": {\n "properties": {\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "description": "Value for relationships.",\n "items": {\n "properties": {\n "type": {\n "description": "Value for type.",\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required authority."\n },\n "required_confidence": {\n "description": "Value for required confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for id."\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for isolation."\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for platform."\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required environment."\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "properties": {\n "disposition": {\n "const": "verified",\n "description": "Value for disposition.",\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "properties": {\n "disposition": {\n "description": "Value for disposition.",\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for resolution."\n },\n "revision": {\n "description": "Value for revision.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "description": "Value for revision digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for scope digest."\n },\n "severity": {\n "description": "Value for severity.",\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "description": "Ordered Evidence identifiers for the supporting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "artifacts",\n "providers",\n "environments",\n "scenarios",\n "captures",\n "unknowns",\n "records"\n ],\n "type": "object"\n },\n "owners": {\n "description": "Value for owners.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "ownership": {\n "description": "Value for ownership.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "authority_route": {\n "description": "Value for authority route.",\n "enum": [\n "none",\n "detected",\n "unknown"\n ],\n "type": "string"\n },\n "disposition": {\n "const": "implemented",\n "description": "Value for disposition.",\n "type": "string"\n },\n "owner_export": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for owner export."\n },\n "owner_path": {\n "description": "Local filesystem path for owner.",\n "minLength": 1,\n "type": "string"\n },\n "owner_sha256": {\n "description": "Exact SHA-256 digest of owner.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "package_state": {\n "description": "Value for package state.",\n "enum": [\n "distributed",\n "missing",\n "unknown"\n ],\n "type": "string"\n },\n "path_state": {\n "description": "Value for path state.",\n "enum": [\n "present",\n "missing",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "owner_path",\n "owner_export",\n "owner_sha256",\n "path_state",\n "package_state",\n "authority_route"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "description": "Value for disposition.",\n "enum": [\n "external",\n "non-goal"\n ],\n "type": "string"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale"\n ],\n "type": "object"\n }\n ]\n },\n "surface_id": {\n "description": "Value for surface id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n }\n },\n "required": [\n "surface_id",\n "ownership"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "package_proofs": {\n "description": "Value for package proofs.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256s": {\n "description": "Value for artifact sha256s.",\n "items": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "kind": {\n "description": "Value for kind.",\n "enum": [\n "build",\n "clean-install",\n "package-contents",\n "runtime-routing",\n "authority-independence"\n ],\n "type": "string"\n },\n "proof_id": {\n "description": "Value for proof id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "pass",\n "fail",\n "unknown",\n "unsupported",\n "skipped"\n ],\n "type": "string"\n }\n },\n "required": [\n "proof_id",\n "kind",\n "status",\n "artifact_sha256s",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "residual_unknown_ids": {\n "description": "Value for residual unknown ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "type": "array"\n },\n "surfaces": {\n "description": "Value for surfaces.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "description": "Value for artifact id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "observed",\n "derived",\n "inferred",\n "historical",\n "external"\n ],\n "type": "string"\n },\n "dependency_surface_ids": {\n "description": "Value for dependency surface ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "type": "array"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "family": {\n "description": "Value for family.",\n "minLength": 1,\n "type": "string"\n },\n "location": {\n "description": "Value for location.",\n "minLength": 1,\n "type": "string"\n },\n "occurrence_id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for occurrence id."\n },\n "surface_id": {\n "description": "Value for surface id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n }\n },\n "required": [\n "surface_id",\n "family",\n "artifact_id",\n "occurrence_id",\n "location",\n "authority",\n "dependency_surface_ids",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "verifier_contracts": {\n "description": "Value for verifier contracts.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "live-observation",\n "external"\n ],\n "type": "string"\n },\n "claim_ids": {\n "description": "Value for claim ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "contract_sha256": {\n "description": "Exact SHA-256 digest of contract.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "dimensions": {\n "description": "Value for dimensions.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "max_age_ms": {\n "description": "Maximum permitted age ms for this operation.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "minimum_repeats": {\n "description": "Value for minimum repeats.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "normalization_removes_dimensions": {\n "const": false,\n "description": "Value for normalization removes dimensions.",\n "type": "boolean"\n },\n "normalization_sha256": {\n "description": "Exact SHA-256 digest of normalization.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "verifier_id": {\n "description": "Value for verifier id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n }\n },\n "required": [\n "verifier_id",\n "claim_ids",\n "dimensions",\n "authority",\n "max_age_ms",\n "minimum_repeats",\n "normalization_sha256",\n "normalization_removes_dimensions",\n "contract_sha256"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "verifier_results": {\n "description": "Value for verifier results.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_sha256s": {\n "description": "Value for artifact sha256s.",\n "items": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "contract_sha256": {\n "description": "Exact SHA-256 digest of contract.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "covered_claim_ids": {\n "description": "Value for covered claim ids.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "covered_dimensions": {\n "description": "Value for covered dimensions.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "normalization_sha256": {\n "description": "Exact SHA-256 digest of normalization.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "observed_at": {\n "description": "Value for observed at.",\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z|([+-](?:[01]\\\\d|2[0-3]):[0-5]\\\\d)))$",\n "type": "string"\n },\n "owner_sha256s": {\n "description": "Value for owner sha256s.",\n "items": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "repeats": {\n "description": "Value for repeats.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "pass",\n "fail",\n "unknown",\n "unsupported",\n "truncated",\n "skipped"\n ],\n "type": "string"\n },\n "verifier_id": {\n "description": "Value for verifier id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n }\n },\n "required": [\n "verifier_id",\n "contract_sha256",\n "observed_at",\n "status",\n "covered_claim_ids",\n "covered_dimensions",\n "artifact_sha256s",\n "owner_sha256s",\n "normalization_sha256",\n "repeats",\n "evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "evidence_bundle",\n "artifacts",\n "surfaces",\n "owners",\n "claims",\n "verifier_contracts",\n "verifier_results",\n "residual_unknown_ids",\n "contradictions",\n "package_proofs",\n "boundaries"\n ],\n "type": "object"\n }\n },\n "required": [\n "coverage",\n "boundary_id"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "evaluate_reconstruction_coverage",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "boundary_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "reasons": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "surface-missing",\n "owner-missing",\n "owner-duplicate",\n "owner-path-missing",\n "owner-path-unknown",\n "owner-undistributed",\n "owner-distribution-unknown",\n "authority-routing-detected",\n "authority-routing-unknown",\n "disposition-not-allowed",\n "dependency-surface-missing",\n "claim-missing",\n "verifier-missing",\n "verifier-duplicate",\n "verifier-result-missing",\n "verifier-result-stale",\n "verifier-result-incompatible",\n "verifier-failed",\n "verifier-unknown",\n "active-unknown",\n "active-contradiction",\n "package-proof-missing",\n "package-proof-failed",\n "package-proof-unknown"\n ],\n "type": "string"\n },\n "detail": {\n "minLength": 1,\n "type": "string"\n },\n "subject_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n }\n },\n "required": [\n "code",\n "subject_id",\n "detail"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "operation": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n },\n "subject_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._:/-]{0,199}$",\n "type": "string"\n }\n },\n "required": [\n "operation",\n "subject_id",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "partial",\n "failed",\n "unknown",\n "ready"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "reasons": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "required_claims": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "required_surfaces": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "required_surfaces",\n "required_claims",\n "reasons"\n ],\n "type": "object"\n }\n },\n "required": [\n "boundary_id",\n "status",\n "summary",\n "reasons",\n "recommended_probes",\n "evidence_ids"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Evaluate Reconstruction Coverage"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Project authenticated APK inventory_artifact Evidence into an execution-free Android application inventory. Reports exact component paths and hashes, runtime-family hints, and path-based bridge hypotheses without decoding DEX, executing the APK, or claiming observed runtime calls.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "inventory_evidence": [\n {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_3cb45ae987278620b8d5a62638296cd49034f8b1dfb88d0196764a1088821e3c",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "edges": [],\n "limitations": [],\n "manifest": {\n "edge_count": 0,\n "graph_sha256": "a4cea3d85dfa8645e5eb026d0dfa82ab19b686bb15964afc5e3f874b14c76de8",\n "manifest_id": "agm_972f60433a8998c511f3b02ee759a41cd1ea2e19ba6c4133b7ac52297944dbb3",\n "node_count": 1,\n "occurrence_count": 1,\n "root_artifact_id": "art_91d55c997599a1b301a52f2ad8559a9f2bfa28927d7cb2c391f3cedf6072ccdd",\n "root_format": "apk",\n "root_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\n },\n "nodes": [\n {\n "architecture": null,\n "artifact_id": "art_91d55c997599a1b301a52f2ad8559a9f2bfa28927d7cb2c391f3cedf6072ccdd",\n "content_state": "materialized",\n "executable": false,\n "format": "apk",\n "kind": "resource",\n "limitations": [],\n "media_type": null,\n "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",\n "size": 1\n }\n ],\n "occurrences": [\n {\n "artifact_id": "art_91d55c997599a1b301a52f2ad8559a9f2bfa28927d7cb2c391f3cedf6072ccdd",\n "compressed_size": null,\n "declared_size": 1,\n "encrypted": false,\n "entry_kind": "file",\n "executable": false,\n "hash_status": "verified",\n "limitations": [],\n "logical_path": ".",\n "occurrence_id": "occ_385f341599a469f680d89371fd8d26bf235e4bd95e29f941f69830343ea72c5a",\n "parent_occurrence_id": null,\n "source_location": null\n }\n ],\n "provenance": []\n },\n "operation": "inventory_artifact",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\n },\n "format": "apk",\n "local_path": "Fixture.apk",\n "name": "Fixture.apk"\n }\n }\n ]\n }\n ],\n "properties": {\n "inventory_evidence": {\n "description": "Value for inventory evidence.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "inventory_evidence"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "project_android_application_graph",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "bridge_candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "managed-and-native-content",\n "jni-library-convention",\n "react-native-convention",\n "flutter-convention",\n "unity-convention"\n ],\n "type": "string"\n },\n "managed_path": {\n "minLength": 1,\n "type": "string"\n },\n "native_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "managed_path",\n "native_path",\n "basis"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "components": {\n "additionalProperties": false,\n "properties": {\n "dex": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "javascript": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "jvm_classes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "manifests": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "native_libraries": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "resources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "signing": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "manifests",\n "resources",\n "dex",\n "jvm_classes",\n "native_libraries",\n "javascript",\n "signing"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "inventory_complete": {\n "type": "boolean"\n },\n "status": {\n "enum": [\n "complete-within-inventory",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "inventory_complete"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "projection_id": {\n "pattern": "^adp_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_format": {\n "const": "apk",\n "type": "string"\n },\n "root_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_families": {\n "items": {\n "enum": [\n "dalvik-art",\n "java-kotlin",\n "native",\n "javascript",\n "react-native",\n "flutter",\n "unity"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "source_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "projection_id",\n "root_sha256",\n "root_format",\n "source_evidence_ids",\n "components",\n "runtime_families",\n "bridge_candidates",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Project Android Application Graph"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Project authenticated IPA, macOS .app directory, ZIP, or DMG inventory Evidence into an execution-free Apple application inventory. Reports application roots, nested bundles with path-convention roles (app extensions, XPC services, login items, privileged helpers, system extensions, frameworks), launchd plists, exact component paths and hashes, runtime-family hints, and path-based bridge hypotheses without parsing plist/CMS semantics or claiming observed runtime calls.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "inventory_evidence": [\n {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_26149f40576cc43ae2d1d8bb465090d2c3b227e3d2394d08cfdf0b61beba64ad",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "edges": [],\n "limitations": [],\n "manifest": {\n "edge_count": 0,\n "graph_sha256": "5e6590160ac53eb4c5dc8a9722eec87dfc6fbee85b8b01c3391b9b5e872c862a",\n "manifest_id": "agm_72d1336aafbec396c67971ffc28c79661b2d1e2bdf8c00823db99ff007f601be",\n "node_count": 1,\n "occurrence_count": 1,\n "root_artifact_id": "art_b42a19aa08c8eda5b54ff7dedca16546a342bfe20da65c1377403feb7a74a960",\n "root_format": "ipa",\n "root_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\n },\n "nodes": [\n {\n "architecture": null,\n "artifact_id": "art_b42a19aa08c8eda5b54ff7dedca16546a342bfe20da65c1377403feb7a74a960",\n "content_state": "materialized",\n "executable": false,\n "format": "ipa",\n "kind": "resource",\n "limitations": [],\n "media_type": null,\n "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",\n "size": 1\n }\n ],\n "occurrences": [\n {\n "artifact_id": "art_b42a19aa08c8eda5b54ff7dedca16546a342bfe20da65c1377403feb7a74a960",\n "compressed_size": null,\n "declared_size": 1,\n "encrypted": false,\n "entry_kind": "file",\n "executable": false,\n "hash_status": "verified",\n "limitations": [],\n "logical_path": ".",\n "occurrence_id": "occ_fc32a68378230630d05886e51a1e327af55b3a1d89f5365e07587e7e9ed1cce6",\n "parent_occurrence_id": null,\n "source_location": null\n }\n ],\n "provenance": []\n },\n "operation": "inventory_artifact",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\n },\n "format": "ipa",\n "local_path": "Fixture.ipa",\n "name": "Fixture.ipa"\n }\n }\n ]\n },\n {\n "inventory_evidence": [\n {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_a2e645cda46d7c9b577779067e00fcd925f515a738014368b977b7be6150b5b3",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "edges": [],\n "limitations": [],\n "manifest": {\n "edge_count": 0,\n "graph_sha256": "239254d4edbe7ca89da697764e02ce8afd6fbb42e5226608fe07757f689b0bdf",\n "manifest_id": "agm_c7b02b31675948c3b62c26dd7e999a141e090e8c9c2b0631e50f3b8863d36911",\n "node_count": 1,\n "occurrence_count": 1,\n "root_artifact_id": "art_f24b49cf6eeac93cf9d7f28b66cfdc83579e311439a543b8301504a12cc20296",\n "root_format": "directory",\n "root_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"\n },\n "nodes": [\n {\n "architecture": null,\n "artifact_id": "art_f24b49cf6eeac93cf9d7f28b66cfdc83579e311439a543b8301504a12cc20296",\n "content_state": "materialized",\n "executable": false,\n "format": "directory",\n "kind": "resource",\n "limitations": [],\n "media_type": null,\n "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",\n "size": 1\n }\n ],\n "occurrences": [\n {\n "artifact_id": "art_f24b49cf6eeac93cf9d7f28b66cfdc83579e311439a543b8301504a12cc20296",\n "compressed_size": null,\n "declared_size": 1,\n "encrypted": false,\n "entry_kind": "file",\n "executable": false,\n "hash_status": "verified",\n "limitations": [],\n "logical_path": ".",\n "occurrence_id": "occ_701e5689a8053d984b5e9e047ac20498755646f679816076be952dfb73aa8b0d",\n "parent_occurrence_id": null,\n "source_location": null\n }\n ],\n "provenance": []\n },\n "operation": "inventory_artifact",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"\n },\n "format": "directory",\n "local_path": "Fixture.app",\n "name": "Fixture.app"\n }\n }\n ]\n }\n ],\n "properties": {\n "inventory_evidence": {\n "description": "Value for inventory evidence.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "inventory_evidence"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "project_apple_application_graph",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "application_roots": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "bridge_candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "javascript-and-native-content",\n "react-native-convention",\n "flutter-convention",\n "unity-convention"\n ],\n "type": "string"\n },\n "native_path": {\n "minLength": 1,\n "type": "string"\n },\n "source_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "source_path",\n "native_path",\n "basis"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "bundles": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "executable_candidates": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "info_plist_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "layout": {\n "enum": [\n "shallow",\n "macos-deep",\n "versioned-framework"\n ],\n "type": "string"\n },\n "parent_path": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "role": {\n "enum": [\n "application",\n "app-extension",\n "xpc-service",\n "framework",\n "login-item",\n "system-extension",\n "driver-extension",\n "plug-in",\n "resource-bundle",\n "bundle",\n "helper-application",\n "nested-application"\n ],\n "type": "string"\n },\n "role_basis": {\n "const": "path-convention",\n "type": "string"\n },\n "signing_paths": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "path",\n "parent_path",\n "layout",\n "role",\n "role_basis",\n "info_plist_path",\n "executable_candidates",\n "signing_paths"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "components": {\n "additionalProperties": false,\n "properties": {\n "bundle_metadata": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "executables": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "frameworks": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "helpers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "javascript": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "launchd_plists": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "domain": {\n "enum": [\n "agent",\n "daemon"\n ],\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format",\n "domain"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "native_libraries": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "privileged_helpers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "signing": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact_id": {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n "format": {\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "artifact_id",\n "sha256",\n "format"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "bundle_metadata",\n "executables",\n "frameworks",\n "native_libraries",\n "javascript",\n "signing",\n "privileged_helpers",\n "launchd_plists",\n "helpers"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "inventory_complete": {\n "type": "boolean"\n },\n "status": {\n "enum": [\n "complete-within-inventory",\n "partial"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "inventory_complete"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "platforms": {\n "items": {\n "enum": [\n "ios",\n "macos"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "projection_id": {\n "pattern": "^aap_[a-f0-9]{64}$",\n "type": "string"\n },\n "root_format": {\n "enum": [\n "ipa",\n "directory",\n "zip",\n "dmg"\n ],\n "type": "string"\n },\n "root_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "runtime_families": {\n "items": {\n "enum": [\n "native",\n "swift-objective-c",\n "javascript",\n "react-native",\n "flutter",\n "unity"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "source_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "symlinks": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "projection_id",\n "root_sha256",\n "root_format",\n "platforms",\n "source_evidence_ids",\n "application_roots",\n "bundles",\n "components",\n "symlinks",\n "runtime_families",\n "bridge_candidates",\n "coverage",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Project Apple Application Graph"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Trace one generated position in an exported website script through an explicitly paired local source map. Verifies manifest, selected script and map SHA-256/byte identities; returns every equal-position mapping, reported source/root, resolved source URL, embedded sourcesContent and original position inline. One-based lines and zero-based UTF-16 columns; greatest generated position <= requested across lines. Duplicate sources and generated-only mappings remain distinct. Caller-selected pairing does not establish deployment authenticity or execution. Regular maps and inline indexed sections only; external section URLs are unsupported. No network access or application execution; uses the pinned upstream codec in an owned Node process with private temporary files, independent 192 MiB old-generation heap and 20-second deadline. 4 MiB map, 32 MiB reply; complete evidence or actionable resource failure. Cleanup may extend the deadline.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "generated_position": {\n "column": 25,\n "line": 1\n },\n "manifest_path": "/analysis/scripts/manifest.json",\n "script_index": 0,\n "source_map": {\n "path": "/analysis/app.js.map",\n "url": "https://example.test/app.js.map"\n }\n }\n ],\n "properties": {\n "generated_position": {\n "additionalProperties": false,\n "description": "Value for generated position.",\n "properties": {\n "column": {\n "description": "Value for column.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "description": "Value for line.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "manifest_path": {\n "description": "Absolute export_web_scripts manifest path",\n "minLength": 1,\n "type": "string"\n },\n "script_index": {\n "description": "Zero-based index in manifest.scripts",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "source_map": {\n "additionalProperties": false,\n "description": "Value for source map.",\n "properties": {\n "path": {\n "description": "Absolute local source-map path; no fetch",\n "minLength": 1,\n "type": "string"\n },\n "url": {\n "description": "Explicit source-map URL context used to resolve original source names",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "url"\n ],\n "type": "object"\n }\n },\n "required": [\n "manifest_path",\n "script_index",\n "source_map",\n "generated_position"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "trace_web_source_location",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "diagnostics": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "version"\n ],\n "type": "object"\n },\n "execution": {\n "const": "unknown",\n "type": "string"\n },\n "format": {\n "enum": [\n "regular",\n "indexed"\n ],\n "type": "string"\n },\n "generated_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "lookup": {\n "additionalProperties": false,\n "properties": {\n "location_units": {\n "const": "one-based lines; zero-based UTF-16 columns",\n "type": "string"\n },\n "matched_generated_position": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "requested": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "line",\n "column"\n ],\n "type": "object"\n },\n "semantics": {\n "const": "greatest generated position <= requested, across lines; all equal-position segments",\n "type": "string"\n }\n },\n "required": [\n "requested",\n "matched_generated_position",\n "semantics",\n "location_units"\n ],\n "type": "object"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "capture_completeness": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n }\n ]\n },\n "capture_kind": {\n "enum": [\n "page-inspection",\n "browser-scenario"\n ],\n "type": "string"\n },\n "capture_path": {\n "minLength": 1,\n "type": "string"\n },\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "reported_output_directory": {\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_evidence_id": {\n "anyOf": [\n {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "reported_output_directory",\n "capture_path",\n "capture_sha256",\n "capture_kind",\n "capture_completeness",\n "source_evidence_id"\n ],\n "type": "object"\n },\n "matches": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "generated-only",\n "type": "string"\n }\n },\n "required": [\n "state"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "content": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "representation": {\n "const": "UTF-8 encoding of decoded sourcesContent text",\n "type": "string"\n },\n "state": {\n "const": "embedded",\n "type": "string"\n },\n "text": {\n "type": "string"\n },\n "utf8_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "utf8_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "state",\n "text",\n "utf8_sha256",\n "utf8_bytes",\n "representation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "const": "sourcesContent-not-retained",\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "flattened_source_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "ignored": {\n "type": "boolean"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "original_position": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "content_position": {\n "enum": [\n "verified-in-embedded-text",\n "outside-embedded-text",\n "content-unavailable"\n ],\n "type": "string"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "offset": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "line",\n "column",\n "offset",\n "content_position"\n ],\n "type": "object"\n },\n "reported_source": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "reported_source_root": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "resolved_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "section_path": {\n "items": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": "array"\n },\n "source_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "mapped",\n "type": "string"\n }\n },\n "required": [\n "state",\n "flattened_source_index",\n "section_path",\n "source_index",\n "reported_source",\n "reported_source_root",\n "resolved_url",\n "ignored",\n "content",\n "original_position",\n "name"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "reported_file": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "runtime": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "v8_heap_limit_bytes": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "version",\n "v8_heap_limit_bytes"\n ],\n "type": "object"\n },\n "source": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "script": {\n "additionalProperties": false,\n "properties": {\n "content": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "layout": {\n "enum": [\n "url-path",\n "isolated"\n ],\n "type": "string"\n },\n "layout_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "redacted": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "relative_path": {\n "minLength": 1,\n "type": "string"\n },\n "representation": {\n "enum": [\n "debugger-source-utf8",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "exported",\n "type": "string"\n }\n },\n "required": [\n "state",\n "relative_path",\n "layout",\n "layout_reason",\n "sha256",\n "bytes",\n "media_type",\n "redacted",\n "representation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n }\n ]\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_module": {\n "type": "boolean"\n },\n "kind": {\n "const": "page-script",\n "type": "string"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "minLength": 1,\n "type": "string"\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "script_key",\n "frame_id",\n "is_module",\n "language",\n "source_map_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "scenario-response",\n "type": "string"\n },\n "request_sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "response_sequence": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "transaction_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "transaction_id",\n "request_sequence",\n "response_sequence",\n "status"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "url",\n "content"\n ],\n "type": "object"\n },\n "script_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "script_index",\n "script"\n ],\n "type": "object"\n },\n "source_authenticity": {\n "const": "unknown",\n "type": "string"\n },\n "source_map": {\n "additionalProperties": false,\n "properties": {\n "association": {\n "const": "caller-selected",\n "type": "string"\n },\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "url",\n "association"\n ],\n "type": "object"\n },\n "source_map_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "url_context": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "source_map_sha256",\n "url_context",\n "engine",\n "runtime",\n "format",\n "reported_file",\n "lookup",\n "matches",\n "diagnostics",\n "manifest",\n "source",\n "source_map",\n "generated_offset",\n "source_authenticity",\n "execution",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Web Source Location"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Trace one exported website script\'s outgoing native ES imports/re-exports, using exact URL and optional selected import-map context. Reads a local export_web_scripts manifest and verifies the selected source\'s SHA-256/size; returns source positions, native resolved URLs or exact errors, every matching captured candidate and explicit unknown execution. Query/fragment identities are preserved. Computed imports and bundler IDs remain unknown. Select importer_url for unknown inline/document-base context. Caller-supplied Chromium executes only a trusted REA resolver stub in an owned context with page requests locally fulfilled/blocked; no captured application execution or asset refetch. Requires absolute REA_BROWSER_EXECUTABLE for literal imports. 32 MiB manifest, 16 MiB source, 4 MiB map and 20-second native deadline; cleanup may extend the deadline.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "import_map": {\n "base_url": "https://example.test/app/",\n "path": "/analysis/import-map.json"\n },\n "manifest_path": "/analysis/exported-scripts/manifest.json",\n "script_index": 0\n }\n ],\n "properties": {\n "import_map": {\n "additionalProperties": false,\n "description": "Value for import map.",\n "properties": {\n "base_url": {\n "description": "Explicit HTTP(S) base for the selected map",\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "description": "Absolute local import-map JSON path; no fetch",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "base_url"\n ],\n "type": "object"\n },\n "importer_url": {\n "description": "Explicit HTTP(S) module URL; useful when captured inline/base context is unknown",\n "minLength": 1,\n "type": "string"\n },\n "manifest_path": {\n "description": "Absolute path to an export_web_scripts manifest",\n "minLength": 1,\n "type": "string"\n },\n "script_index": {\n "description": "Zero-based index in manifest.scripts",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "manifest_path",\n "script_index"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "trace_web_module_imports",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "diagnostics": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "engine": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "version"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "import_map": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "base_url": {\n "minLength": 1,\n "type": "string"\n },\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "base_url"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "importer": {\n "additionalProperties": false,\n "properties": {\n "basis": {\n "enum": [\n "caller-selected",\n "reported-source-url"\n ],\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "url",\n "basis"\n ],\n "type": "object"\n },\n "imports": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "captured_candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "match": {\n "enum": [\n "exact-reported-url",\n "response-url-without-fragment"\n ],\n "type": "string"\n },\n "script": {\n "additionalProperties": false,\n "properties": {\n "content": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "layout": {\n "enum": [\n "url-path",\n "isolated"\n ],\n "type": "string"\n },\n "layout_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "redacted": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "relative_path": {\n "minLength": 1,\n "type": "string"\n },\n "representation": {\n "enum": [\n "debugger-source-utf8",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "exported",\n "type": "string"\n }\n },\n "required": [\n "state",\n "relative_path",\n "layout",\n "layout_reason",\n "sha256",\n "bytes",\n "media_type",\n "redacted",\n "representation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n }\n ]\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_module": {\n "type": "boolean"\n },\n "kind": {\n "const": "page-script",\n "type": "string"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "minLength": 1,\n "type": "string"\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "script_key",\n "frame_id",\n "is_module",\n "language",\n "source_map_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "scenario-response",\n "type": "string"\n },\n "request_sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "response_sequence": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "transaction_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "transaction_id",\n "request_sequence",\n "response_sequence",\n "status"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "url",\n "content"\n ],\n "type": "object"\n },\n "script_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "script_index",\n "match",\n "script"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "end": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "offset",\n "line",\n "column"\n ],\n "type": "object"\n },\n "execution": {\n "const": "unknown",\n "type": "string"\n },\n "expression": {\n "type": "string"\n },\n "kind": {\n "enum": [\n "static-import",\n "re-export",\n "dynamic-import"\n ],\n "type": "string"\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "resolved",\n "type": "string"\n },\n "url": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "state",\n "url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "type": "string"\n },\n "state": {\n "const": "rejected",\n "type": "string"\n }\n },\n "required": [\n "state",\n "message"\n ],\n "type": "object"\n }\n ]\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "const": "computed-specifier",\n "type": "string"\n },\n "state": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "specifier": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "start": {\n "additionalProperties": false,\n "properties": {\n "column": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "offset",\n "line",\n "column"\n ],\n "type": "object"\n }\n },\n "required": [\n "kind",\n "specifier",\n "expression",\n "start",\n "end",\n "resolution",\n "captured_candidates",\n "execution"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "capture_completeness": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n }\n ]\n },\n "capture_kind": {\n "enum": [\n "page-inspection",\n "browser-scenario"\n ],\n "type": "string"\n },\n "capture_path": {\n "minLength": 1,\n "type": "string"\n },\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "reported_output_directory": {\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "source_evidence_id": {\n "anyOf": [\n {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "reported_output_directory",\n "capture_path",\n "capture_sha256",\n "capture_kind",\n "capture_completeness",\n "source_evidence_id"\n ],\n "type": "object"\n },\n "parser": {\n "additionalProperties": false,\n "properties": {\n "diagnostics": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "location_units": {\n "const": "UTF-16 offsets and columns; one-based lines",\n "type": "string"\n },\n "state": {\n "enum": [\n "parsed",\n "unparseable"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "diagnostics",\n "location_units"\n ],\n "type": "object"\n },\n "source": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "script": {\n "additionalProperties": false,\n "properties": {\n "content": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "layout": {\n "enum": [\n "url-path",\n "isolated"\n ],\n "type": "string"\n },\n "layout_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "redacted": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "relative_path": {\n "minLength": 1,\n "type": "string"\n },\n "representation": {\n "enum": [\n "debugger-source-utf8",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "exported",\n "type": "string"\n }\n },\n "required": [\n "state",\n "relative_path",\n "layout",\n "layout_reason",\n "sha256",\n "bytes",\n "media_type",\n "redacted",\n "representation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n }\n ]\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_module": {\n "type": "boolean"\n },\n "kind": {\n "const": "page-script",\n "type": "string"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "minLength": 1,\n "type": "string"\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "script_key",\n "frame_id",\n "is_module",\n "language",\n "source_map_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "scenario-response",\n "type": "string"\n },\n "request_sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "response_sequence": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "transaction_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "transaction_id",\n "request_sequence",\n "response_sequence",\n "status"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "url",\n "content"\n ],\n "type": "object"\n },\n "script_index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "script_index",\n "script"\n ],\n "type": "object"\n }\n },\n "required": [\n "manifest",\n "source",\n "importer",\n "import_map",\n "parser",\n "engine",\n "imports",\n "diagnostics",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Trace Web Module Imports"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Export JavaScript bytes already retained in one local inspect_web_page or capture_browser_scenario JSON capture (normalized result or complete Evidence). Writes scripts and a digest-verified manifest into an absent absolute output_directory. Returns all source URLs, script or transaction/event references, unavailable states, and an analysis_input for analyze_javascript_application when any bytes were exported. Include script sources in inspect_web_page or select capture.network.response_body in scenarios. No refetch or execution. Safe unambiguous URL layouts preserve relative module paths; versions, query variants, inline scripts, and path collisions are isolated with explicit resolution limitations.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "capture_path": "/analysis/capture.json",\n "output_directory": "/analysis/exported-scripts"\n }\n ],\n "properties": {\n "capture_path": {\n "description": "Absolute filesystem path to saved inspect_web_page or capture_browser_scenario JSON, normalized result or complete Evidence",\n "minLength": 1,\n "type": "string"\n },\n "output_directory": {\n "description": "Absolute absent filesystem directory for verified scripts and manifest; parent directory must exist",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "capture_path",\n "output_directory"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "export_web_scripts",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "analysis_input": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "format": {\n "const": "directory",\n "type": "string"\n },\n "input_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "input_path",\n "format"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "capture_completeness": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "attach_limited_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "conditions": {\n "items": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "maxItems": 3,\n "minItems": 1,\n "type": "array"\n },\n "dropped_events": {\n "additionalProperties": false,\n "properties": {\n "console_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "network_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "timeline_events": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "webmcp_tools": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_connections": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "websocket_frames": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "scripts",\n "network_requests",\n "console_events",\n "websocket_connections",\n "websocket_frames",\n "webmcp_tools",\n "timeline_events",\n "total"\n ],\n "type": "object"\n },\n "excluded": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "disallowed_origin",\n "unsupported_url",\n "unattributed_origin",\n "not_approved",\n "out_of_target_scope",\n "provider_unavailable",\n "invalid_protocol_value"\n ],\n "type": "string"\n },\n "section": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n }\n },\n "required": [\n "section",\n "reason",\n "count"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "policy_filtered_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "complete_within_window",\n "policy_filtered",\n "attach_limited",\n "truncated"\n ],\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "unavailable_sections": {\n "items": {\n "enum": [\n "frames",\n "dom",\n "accessibility",\n "scripts",\n "script_sources",\n "resources",\n "network_requests",\n "network_initiators",\n "console_events",\n "console_text",\n "websocket_connections",\n "websocket_frames",\n "websocket_shapes",\n "json_body_shapes",\n "workers",\n "storage",\n "storage_keys",\n "metadata",\n "webmcp_tools",\n "screenshot",\n "source_maps",\n "timeline"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "status",\n "conditions",\n "policy_filtered_sections",\n "attach_limited_sections",\n "truncated_sections",\n "unavailable_sections",\n "excluded",\n "dropped_events"\n ],\n "type": "object"\n },\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": true,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "status": {\n "const": "complete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "const": "incomplete",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "equality_eligible": {\n "const": false,\n "type": "boolean"\n },\n "missing_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "truncated",\n "type": "string"\n },\n "truncated_sections": {\n "items": {\n "enum": [\n "action",\n "screenshot",\n "dom",\n "accessibility",\n "url",\n "history",\n "storage",\n "events",\n "frames",\n "workers",\n "popups",\n "websockets"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "status",\n "equality_eligible",\n "missing_sections",\n "truncated_sections"\n ],\n "type": "object"\n }\n ]\n }\n ]\n },\n "capture_kind": {\n "enum": [\n "page-inspection",\n "browser-scenario"\n ],\n "type": "string"\n },\n "capture_path": {\n "minLength": 1,\n "type": "string"\n },\n "capture_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "output_directory": {\n "minLength": 1,\n "type": "string"\n },\n "scripts": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "content": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "layout": {\n "enum": [\n "url-path",\n "isolated"\n ],\n "type": "string"\n },\n "layout_reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "media_type": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "redacted": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "relative_path": {\n "minLength": 1,\n "type": "string"\n },\n "representation": {\n "enum": [\n "debugger-source-utf8",\n "browser-decoded-response-bytes"\n ],\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "exported",\n "type": "string"\n }\n },\n "required": [\n "state",\n "relative_path",\n "layout",\n "layout_reason",\n "sha256",\n "bytes",\n "media_type",\n "redacted",\n "representation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "message": {\n "minLength": 1,\n "type": "string"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "message"\n ],\n "type": "object"\n }\n ]\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "is_module": {\n "type": "boolean"\n },\n "kind": {\n "const": "page-script",\n "type": "string"\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_key": {\n "minLength": 1,\n "type": "string"\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "script_key",\n "frame_id",\n "is_module",\n "language",\n "source_map_url"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "scenario-response",\n "type": "string"\n },\n "request_sequence": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "response_sequence": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "anyOf": [\n {\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "transaction_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "kind",\n "transaction_id",\n "request_sequence",\n "response_sequence",\n "status"\n ],\n "type": "object"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "source",\n "url",\n "content"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "source_evidence_id": {\n "anyOf": [\n {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "capture_path",\n "capture_sha256",\n "capture_kind",\n "source_evidence_id",\n "capture_completeness",\n "output_directory",\n "analysis_input",\n "scripts",\n "limitations",\n "manifest"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Export Web Scripts"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Instrument one selected CDP page for a finite, externally driven precise coverage window. Resets execution counters and prevents optimized execution until stopped. Returns complete nested UTF-16 ranges/counts, independently hashed script text and request initiator associations by session script ID. Does not navigate, click, evaluate selected code or close the externally owned page. Navigation ends document-scoped collection; workers and UI causality remain outside this claim. Emits an actual armed progress notification before external actions.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": true,\n "mutatesSession": true,\n "mutatesTarget": true,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "observation_ms": 10000,\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "observation_ms": {\n "default": 10000,\n "description": "Value for observation ms.",\n "maximum": 2147483647,\n "minimum": 1,\n "type": "integer"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "observe_web_execution",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "coverage": {\n "additionalProperties": false,\n "properties": {\n "end_offset": {\n "const": "exclusive",\n "type": "string"\n },\n "excluded_scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "offset_units": {\n "const": "utf16-code-units",\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "scripts": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "functions": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "is_block_coverage": {\n "type": "boolean"\n },\n "name": {\n "type": "string"\n },\n "ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "count": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "end_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "source_bounds": {\n "enum": [\n "verified",\n "unknown"\n ],\n "type": "string"\n },\n "start_offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "start_offset",\n "end_offset",\n "count",\n "source_bounds"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "name",\n "is_block_coverage",\n "ranges"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "reported_url": {\n "type": "string"\n },\n "script_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "script_id",\n "reported_url",\n "functions"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "state": {\n "enum": [\n "captured",\n "unavailable"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "offset_units",\n "end_offset",\n "scripts",\n "excluded_scripts"\n ],\n "type": "object"\n },\n "excluded_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "instrumentation": {\n "additionalProperties": false,\n "properties": {\n "cleanup": {\n "const": "confirmed",\n "type": "string"\n },\n "disables_optimized_execution": {\n "const": true,\n "type": "boolean"\n },\n "executes_selected_code": {\n "const": false,\n "type": "boolean"\n },\n "page_ownership": {\n "const": "external",\n "type": "string"\n },\n "resets_execution_counters": {\n "const": true,\n "type": "boolean"\n },\n "takes_one_resetting_sample": {\n "type": "boolean"\n }\n },\n "required": [\n "resets_execution_counters",\n "disables_optimized_execution",\n "takes_one_resetting_sample",\n "executes_selected_code",\n "cleanup",\n "page_ownership"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "requests": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "async_parent_ids": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "debugger_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "type": "string"\n }\n },\n "required": [\n "id",\n "debugger_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "callsites": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "column_number": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "function_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "line_number": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "script_id": {\n "type": "string"\n },\n "source_association": {\n "enum": [\n "script_id",\n "unknown"\n ],\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "script_id",\n "url",\n "line_number",\n "column_number",\n "function_name",\n "source_association"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "causal_attribution": {\n "const": "unknown",\n "type": "string"\n },\n "initiator_type": {\n "type": "string"\n },\n "method": {\n "type": "string"\n },\n "reported_initiator": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "request_id": {\n "minLength": 1,\n "type": "string"\n },\n "timestamp_seconds": {\n "minimum": 0,\n "type": "number"\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "request_id",\n "url",\n "method",\n "timestamp_seconds",\n "initiator_type",\n "reported_initiator",\n "callsites",\n "async_parent_ids",\n "causal_attribution"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "script_inventory": {\n "additionalProperties": false,\n "properties": {\n "coverage_absence": {\n "const": "unknown",\n "type": "string"\n },\n "main_document_scripts": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "not_reported_script_ids": {\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "main_document_scripts",\n "not_reported_script_ids",\n "coverage_absence"\n ],\n "type": "object"\n },\n "sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "execution_context_id": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "has_source_url": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "producer_hash": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "reported_execution_context": {\n "anyOf": [\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "reported_script_context_aux_data": {\n "anyOf": [\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_start": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "column_number": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line_number": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "line_number",\n "column_number"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_id": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "captured",\n "type": "string"\n },\n "text": {\n "type": "string"\n },\n "utf16_units": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "utf8_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "state",\n "text",\n "sha256",\n "utf8_bytes",\n "utf16_units"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "enum": [\n "unavailable",\n "excluded"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "script_id",\n "url",\n "execution_context_id",\n "reported_execution_context",\n "reported_script_context_aux_data",\n "frame_id",\n "producer_hash",\n "source_map_url",\n "has_source_url",\n "language",\n "resource_start",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "minLength": 1,\n "type": "string"\n },\n "initial_url": {\n "type": "string"\n },\n "loader_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "origin": {\n "minLength": 1,\n "type": "string"\n },\n "target_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "origin",\n "initial_url",\n "frame_id",\n "loader_id"\n ],\n "type": "object"\n },\n "window": {\n "additionalProperties": false,\n "properties": {\n "armed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "end_reason": {\n "enum": [\n "window_elapsed",\n "document_changed",\n "target_terminated"\n ],\n "type": "string"\n },\n "ended_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "producer_sampled_seconds": {\n "anyOf": [\n {\n "minimum": 0,\n "type": "number"\n },\n {\n "type": "null"\n }\n ]\n },\n "producer_started_seconds": {\n "minimum": 0,\n "type": "number"\n },\n "requested_ms": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "armed_at",\n "ended_at",\n "requested_ms",\n "end_reason",\n "producer_started_seconds",\n "producer_sampled_seconds"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "window",\n "coverage",\n "requests",\n "excluded_requests",\n "script_inventory",\n "sources",\n "instrumentation",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Observe Web Execution"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Inspect listeners registered directly on the first main-document CSS match in one selected CDP page. Returns event flags, native callback locations, source ownership and exact script text/digests inline. Does not dispatch events, evaluate JavaScript or invoke handlers. Delegated ancestor/framework listeners and execution history remain unknown. Releases REA\'s object group and transport, preserving the external page.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "cdp_endpoint": "http://127.0.0.1:9222",\n "selector": "#submit",\n "target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"\n }\n ],\n "properties": {\n "allowed_origins": {\n "description": "Value for allowed origins.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "cdp_endpoint": {\n "description": "Literal loopback Chrome DevTools Protocol endpoint.",\n "minLength": 1,\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "minLength": 1,\n "type": "string"\n },\n "target_id": {\n "description": "Exact CDP target identifier selected from the endpoint\'s target listing.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "cdp_endpoint",\n "target_id",\n "selector"\n ],\n "type": "object"\n },\n "kind": "browser-provider",\n "name": "inspect_web_event_listeners",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "browser": {\n "additionalProperties": false,\n "properties": {\n "js_version": {\n "type": "string"\n },\n "product": {\n "type": "string"\n },\n "protocol_version": {\n "type": "string"\n },\n "revision": {\n "type": "string"\n },\n "user_agent": {\n "type": "string"\n }\n },\n "required": [\n "product",\n "protocol_version",\n "revision",\n "user_agent",\n "js_version"\n ],\n "type": "object"\n },\n "cleanup": {\n "const": "confirmed",\n "type": "string"\n },\n "inspected_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "listeners": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "execution": {\n "const": "unknown",\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "column_number": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "function_name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "line_number": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "script_id": {\n "type": "string"\n },\n "source_association": {\n "enum": [\n "script_id",\n "unknown"\n ],\n "type": "string"\n },\n "url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "script_id",\n "url",\n "line_number",\n "column_number",\n "function_name",\n "source_association"\n ],\n "type": "object"\n },\n "once": {\n "type": "boolean"\n },\n "passive": {\n "type": "boolean"\n },\n "type": {\n "type": "string"\n },\n "use_capture": {\n "type": "boolean"\n }\n },\n "required": [\n "type",\n "use_capture",\n "passive",\n "once",\n "location",\n "execution"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "selected_node": {\n "additionalProperties": false,\n "properties": {\n "backend_node_id": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "match": {\n "enum": [\n "found",\n "not_found"\n ],\n "type": "string"\n },\n "selector": {\n "type": "string"\n }\n },\n "required": [\n "selector",\n "match",\n "backend_node_id"\n ],\n "type": "object"\n },\n "sources": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "execution_context_id": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "frame_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "has_source_url": {\n "anyOf": [\n {\n "type": "boolean"\n },\n {\n "type": "null"\n }\n ]\n },\n "language": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "producer_hash": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "reported_execution_context": {\n "anyOf": [\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "reported_script_context_aux_data": {\n "anyOf": [\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resource_start": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "column_number": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "line_number": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "line_number",\n "column_number"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "script_id": {\n "minLength": 1,\n "type": "string"\n },\n "source": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "state": {\n "const": "captured",\n "type": "string"\n },\n "text": {\n "type": "string"\n },\n "utf16_units": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "utf8_bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "state",\n "text",\n "sha256",\n "utf8_bytes",\n "utf16_units"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "state": {\n "enum": [\n "unavailable",\n "excluded"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason"\n ],\n "type": "object"\n }\n ]\n },\n "source_map_url": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "url": {\n "type": "string"\n }\n },\n "required": [\n "script_id",\n "url",\n "execution_context_id",\n "reported_execution_context",\n "reported_script_context_aux_data",\n "frame_id",\n "producer_hash",\n "source_map_url",\n "has_source_url",\n "language",\n "resource_start",\n "source"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "target": {\n "additionalProperties": false,\n "properties": {\n "frame_id": {\n "minLength": 1,\n "type": "string"\n },\n "initial_url": {\n "type": "string"\n },\n "loader_id": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "origin": {\n "minLength": 1,\n "type": "string"\n },\n "target_id": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "target_id",\n "origin",\n "initial_url",\n "frame_id",\n "loader_id"\n ],\n "type": "object"\n }\n },\n "required": [\n "browser",\n "target",\n "inspected_at",\n "selected_node",\n "listeners",\n "sources",\n "cleanup",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Inspect Web Event Listeners"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Recover readable modules from one local UTF-8 JavaScript script/bundle into an absent directory, using caller-supplied Wakaru 1.13.0 on Linux x64. Returns original/derived digests, extraction byte ranges, emitted source maps, warnings and analysis_input inline. Structural splitting may fall back to one module; original names and runtime equivalence remain unknown. No application code execution. Requires util-linux prlimit; one worker, 1 GiB address-space ceiling, 120-second deadline, 64 MiB input, 128 MiB output, 10000 entries and 8 MiB per diagnostic/report stream.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "output_directory": "/tmp/recovered-app",\n "path": "/tmp/web-scripts/app.js"\n }\n ],\n "properties": {\n "extraction_mode": {\n "default": "structural",\n "description": "Structural module boundaries, heuristic fallback, or finer inspection-only regions",\n "enum": [\n "structural",\n "heuristic",\n "inspection"\n ],\n "type": "string"\n },\n "output_directory": {\n "description": "Absolute absent output directory; parent must exist",\n "minLength": 1,\n "type": "string"\n },\n "path": {\n "description": "Absolute path to a UTF-8 JavaScript file; never executed",\n "minLength": 1,\n "type": "string"\n },\n "rewrite_level": {\n "default": "standard",\n "description": "Upstream syntax recovery level; runtime equivalence remains unverified",\n "enum": [\n "minimal",\n "standard",\n "aggressive"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "output_directory"\n ],\n "type": "object"\n },\n "kind": "application",\n "name": "recover_javascript_sources",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "analysis_input": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "format": {\n "const": "directory",\n "type": "string"\n },\n "input_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "input_path",\n "format"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "detected_formats": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "engine": {\n "additionalProperties": false,\n "properties": {\n "audited_source_revision": {\n "minLength": 1,\n "type": "string"\n },\n "executable": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "executed_source_revision": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "version",\n "executable",\n "audited_source_revision",\n "executed_source_revision"\n ],\n "type": "object"\n },\n "failed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "modules": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "artifact": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "provenance": {\n "additionalProperties": false,\n "properties": {\n "byte_ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "context_byte_ranges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "start",\n "end"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "extraction": {\n "type": "string"\n },\n "original_path": {\n "type": "string"\n },\n "original_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "range_semantics": {\n "const": "half-open-utf8-bytes",\n "type": "string"\n },\n "reported_input": {\n "type": "string"\n }\n },\n "required": [\n "reported_input",\n "original_path",\n "original_sha256",\n "extraction",\n "byte_ranges",\n "context_byte_ranges",\n "range_semantics"\n ],\n "type": "object"\n },\n "reported_filename": {\n "minLength": 1,\n "type": "string"\n },\n "reported_status": {\n "minLength": 1,\n "type": "string"\n },\n "source_map": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "reported_filename",\n "reported_status",\n "artifact",\n "source_map",\n "provenance"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "options": {\n "additionalProperties": false,\n "properties": {\n "extraction_mode": {\n "enum": [\n "structural",\n "heuristic",\n "inspection"\n ],\n "type": "string"\n },\n "rewrite_level": {\n "enum": [\n "minimal",\n "standard",\n "aggressive"\n ],\n "type": "string"\n }\n },\n "required": [\n "extraction_mode",\n "rewrite_level"\n ],\n "type": "object"\n },\n "provenance": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "report": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "reported_format": {\n "type": "string"\n },\n "reported_safety": {\n "type": "string"\n },\n "reported_strategy": {\n "type": "string"\n },\n "runtime_equivalence": {\n "const": "unknown",\n "type": "string"\n },\n "source": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "published_copy": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes"\n ],\n "type": "object"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "snapshot_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path",\n "sha256",\n "bytes",\n "snapshot_path",\n "published_copy"\n ],\n "type": "object"\n },\n "status": {\n "enum": [\n "complete",\n "partial"\n ],\n "type": "string"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "warnings": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "filename": {\n "type": "string"\n },\n "is_error": {\n "type": "boolean"\n },\n "kind": {\n "type": "string"\n },\n "message": {\n "type": "string"\n }\n },\n "required": [\n "filename",\n "kind",\n "is_error",\n "message"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "source",\n "engine",\n "options",\n "status",\n "detected_formats",\n "reported_safety",\n "reported_strategy",\n "reported_format",\n "total",\n "failed",\n "modules",\n "warnings",\n "report",\n "provenance",\n "manifest",\n "analysis_input",\n "runtime_equivalence",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": false,\n "title": "Recover Javascript Sources"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Open a local executable, application bundle, archive, JavaScript, source map, plist, or analysis database after validation. format=dos-com explicitly interprets 1..65280 headerless bytes as a DOS COM analysis image; omission preserves header-based detection. provider_id selects one deep provider or deterministic auto selection; the binding remains stable until close or an explicit switch, with no failure fallback. An optional analysis snapshot is imported atomically and must match the binary identity, concrete provider, and canonical analysis profile exactly.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "path": "/tmp/fixture"\n }\n ],\n "properties": {\n "format": {\n "const": "dos-com",\n "description": "Explicit headerless DOS COM interpretation; omission preserves header-based detection",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "provider_id": {\n "anyOf": [\n {\n "const": "auto",\n "type": "string"\n },\n {\n "minLength": 1,\n "pattern": "^[a-z][a-z0-9-]*$",\n "type": "string"\n }\n ],\n "description": "Exact deep-analysis provider ID, or automatic selection when omitted."\n },\n "snapshot_path": {\n "description": "Local filesystem path for snapshot.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "open_binary",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map"\n ],\n "type": "string"\n },\n "kind": {\n "enum": [\n "executable",\n "database",\n "archive",\n "artifact"\n ],\n "type": "string"\n },\n "loaderArgs": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "path",\n "format",\n "kind",\n "loaderArgs",\n "sha256",\n "architecture"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Open Binary"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Optionally write a provider-neutral analysis snapshot atomically to the caller-supplied path, then close the active target and every provider resource started for it. Existing files require explicit overwrite; a failed save leaves the session open so cached analysis is not lost.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": true,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "overwrite": false\n }\n ],\n "properties": {\n "overwrite": {\n "default": false,\n "description": "Whether an existing destination may be replaced.",\n "type": "boolean"\n },\n "snapshot_path": {\n "description": "Local filesystem path for snapshot.",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "session",\n "name": "close_binary",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "anyOf": [\n {\n "type": "null"\n },\n {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "entries": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n }\n },\n "required": [\n "path",\n "bytes",\n "entries"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Close Binary"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": true\n },\n "description": "Report the complete current target, provider, capability availability, client-feature, analysis, and server-identity status without starting analysis.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": false,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {}\n ],\n "properties": {\n "expected_catalog_digest": {\n "description": "Expected catalog digest used to reject stale or mismatched input.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "expected_package_version": {\n "description": "Expected package version used to reject stale or mismatched input.",\n "minLength": 1,\n "type": "string"\n },\n "expected_server_path": {\n "description": "Expected server path used to reject stale or mismatched input.",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "session",\n "name": "binary_session",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n },\n "__schema1": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema1"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "anyOf": [\n {\n "anyOf": [\n {\n "additionalProperties": {},\n "properties": {\n "analysis_activity": {\n "additionalProperties": false,\n "properties": {\n "providers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "active": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "caller_state": {\n "enum": [\n "waiting",\n "cancelled"\n ],\n "type": "string"\n },\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "request_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "request_id",\n "operation",\n "elapsed_ms",\n "caller_state"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "queued_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "provider",\n "active",\n "queued_requests"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "not_observed",\n "idle",\n "busy"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "providers"\n ],\n "type": "object"\n },\n "analysis_provider_binding": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "selection_source": {\n "enum": [\n "request",\n "environment",\n "auto-single-candidate"\n ],\n "type": "string"\n }\n },\n "required": [\n "provider",\n "selection_source",\n "analysis_profile"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "analysis_provider_candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "type": "null"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "available",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "not_configured",\n "executable_missing",\n "runtime_missing",\n "unsupported_host",\n "unsupported_version",\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported",\n "open_options_invalid",\n "version_unresolved"\n ],\n "type": "string"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n }\n ]\n },\n "capabilities": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "type": "null"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "type": "null"\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "anyOf": [\n {\n "enum": [\n "not_configured",\n "executable_missing",\n "runtime_missing",\n "unsupported_host",\n "unsupported_version",\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported",\n "open_options_invalid",\n "version_unresolved"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "available": {\n "const": false,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "selected": {\n "type": "boolean"\n },\n "target_support": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "type": "null"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "type": "null"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "supported",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported"\n ],\n "type": "string"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unsupported",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "provider",\n "availability",\n "target_support",\n "selected",\n "capabilities"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "analysis_run": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "process_lineage": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "status": {\n "const": "not_observed",\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "snapshots": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observation": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "launcher_pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "observed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "process_group_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "status",\n "observed_at",\n "launcher_pid",\n "process_group_id",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "descendants": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "parent_pid": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "process_group_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "parent_pid",\n "process_group_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "launcher_parent_pid": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "launcher_pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "observed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "process_group_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "const": "verified",\n "type": "string"\n }\n },\n "required": [\n "status",\n "observed_at",\n "launcher_pid",\n "process_group_id",\n "launcher_parent_pid",\n "descendants"\n ],\n "type": "object"\n }\n ]\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "observation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "const": "snapshots",\n "type": "string"\n }\n },\n "required": [\n "status",\n "snapshots"\n ],\n "type": "object"\n }\n ]\n },\n "run_id": {\n "format": "uuid",\n "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",\n "type": "string"\n }\n },\n "required": [\n "run_id",\n "process_lineage"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "capabilities": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "type": "null"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "type": "null"\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "anyOf": [\n {\n "enum": [\n "not_configured",\n "executable_missing",\n "runtime_missing",\n "unsupported_host",\n "unsupported_version",\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported",\n "open_options_invalid",\n "version_unresolved"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "available": {\n "const": false,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "client_features": {\n "additionalProperties": false,\n "properties": {\n "elicitation_form": {\n "type": "boolean"\n },\n "elicitation_url": {\n "type": "boolean"\n },\n "roots": {\n "type": "boolean"\n },\n "sampling": {\n "type": "boolean"\n }\n },\n "required": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "object"\n },\n "open": {\n "const": false,\n "type": "boolean"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "providers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "server_identity": {\n "additionalProperties": false,\n "properties": {\n "alignment": {\n "additionalProperties": false,\n "properties": {\n "reasons": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "state": {\n "enum": [\n "aligned",\n "mcp_server_restart_required",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "reasons",\n "remediation"\n ],\n "type": "object"\n },\n "catalog": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "client": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "negotiated_protocol_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "package": {\n "additionalProperties": false,\n "properties": {\n "build_commit": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "root_path": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "root_path",\n "build_commit"\n ],\n "type": "object"\n },\n "protocol_features": {\n "additionalProperties": false,\n "properties": {\n "cancellation": {\n "type": "boolean"\n },\n "elicitation": {\n "type": "boolean"\n },\n "progress": {\n "type": "boolean"\n }\n },\n "required": [\n "progress",\n "cancellation",\n "elicitation"\n ],\n "type": "object"\n },\n "sdk": {\n "additionalProperties": false,\n "properties": {\n "client_test": {\n "type": "string"\n },\n "core": {\n "type": "string"\n },\n "server": {\n "type": "string"\n }\n },\n "required": [\n "server",\n "client_test",\n "core"\n ],\n "type": "object"\n },\n "server": {\n "additionalProperties": false,\n "properties": {\n "command_path": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "started_at": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "started_at",\n "command_path"\n ],\n "type": "object"\n },\n "skill": {\n "additionalProperties": false,\n "properties": {\n "expected_version": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "expected_version"\n ],\n "type": "object"\n }\n },\n "required": [\n "package",\n "server",\n "sdk",\n "negotiated_protocol_version",\n "client",\n "skill",\n "catalog",\n "protocol_features",\n "alignment"\n ],\n "type": "object"\n },\n "tool_availability": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "annotations": {\n "additionalProperties": false,\n "properties": {\n "destructive": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "open_world": {\n "type": "boolean"\n },\n "read_only": {\n "type": "boolean"\n }\n },\n "required": [\n "read_only",\n "destructive",\n "idempotent",\n "open_world"\n ],\n "type": "object"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "client_requirements": {\n "additionalProperties": false,\n "properties": {\n "missing_optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "missing_required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "required",\n "optional",\n "missing_required",\n "missing_optional"\n ],\n "type": "object"\n },\n "default_mode_available": {\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "accessesNetwork": {\n "type": "boolean"\n },\n "changesUiState": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "launchesProcess": {\n "type": "boolean"\n },\n "mayDiscardData": {\n "type": "boolean"\n },\n "mutatesSession": {\n "type": "boolean"\n },\n "mutatesTarget": {\n "type": "boolean"\n },\n "writesFilesystem": {\n "type": "boolean"\n }\n },\n "required": [\n "mutatesTarget",\n "mutatesSession",\n "writesFilesystem",\n "launchesProcess",\n "accessesNetwork",\n "changesUiState",\n "mayDiscardData",\n "idempotent"\n ],\n "type": "object"\n },\n "modes": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "type": "null"\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "reason": {\n "const": "available",\n "type": "string"\n },\n "remediation": {\n "type": "null"\n },\n "surface": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "surface",\n "client_requirements",\n "effects",\n "annotations",\n "available",\n "reason",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "annotations": {\n "additionalProperties": false,\n "properties": {\n "destructive": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "open_world": {\n "type": "boolean"\n },\n "read_only": {\n "type": "boolean"\n }\n },\n "required": [\n "read_only",\n "destructive",\n "idempotent",\n "open_world"\n ],\n "type": "object"\n },\n "available": {\n "const": false,\n "type": "boolean"\n },\n "client_requirements": {\n "additionalProperties": false,\n "properties": {\n "missing_optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "missing_required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "required",\n "optional",\n "missing_required",\n "missing_optional"\n ],\n "type": "object"\n },\n "default_mode_available": {\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "accessesNetwork": {\n "type": "boolean"\n },\n "changesUiState": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "launchesProcess": {\n "type": "boolean"\n },\n "mayDiscardData": {\n "type": "boolean"\n },\n "mutatesSession": {\n "type": "boolean"\n },\n "mutatesTarget": {\n "type": "boolean"\n },\n "writesFilesystem": {\n "type": "boolean"\n }\n },\n "required": [\n "mutatesTarget",\n "mutatesSession",\n "writesFilesystem",\n "launchesProcess",\n "accessesNetwork",\n "changesUiState",\n "mayDiscardData",\n "idempotent"\n ],\n "type": "object"\n },\n "modes": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "type": "null"\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "reason": {\n "enum": [\n "client_capability_missing",\n "target_required",\n "provider_missing",\n "provider_unavailable",\n "target_unsupported",\n "unsupported_host"\n ],\n "type": "string"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "surface": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "surface",\n "client_requirements",\n "effects",\n "annotations",\n "available",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "provider",\n "providers",\n "capabilities",\n "analysis_run",\n "analysis_activity",\n "analysis_provider_binding",\n "analysis_provider_candidates",\n "tool_availability",\n "client_features",\n "server_identity",\n "open"\n ],\n "type": "object"\n },\n {\n "additionalProperties": {},\n "properties": {\n "analysis_activity": {\n "additionalProperties": false,\n "properties": {\n "providers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "active": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "caller_state": {\n "enum": [\n "waiting",\n "cancelled"\n ],\n "type": "string"\n },\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "request_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "request_id",\n "operation",\n "elapsed_ms",\n "caller_state"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "queued_requests": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "provider",\n "active",\n "queued_requests"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "not_observed",\n "idle",\n "busy"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "providers"\n ],\n "type": "object"\n },\n "analysis_provider_binding": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "selection_source": {\n "enum": [\n "request",\n "environment",\n "auto-single-candidate"\n ],\n "type": "string"\n }\n },\n "required": [\n "provider",\n "selection_source",\n "analysis_profile"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "analysis_provider_candidates": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "availability": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "type": "null"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "available",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "not_configured",\n "executable_missing",\n "runtime_missing",\n "unsupported_host",\n "unsupported_version",\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported",\n "open_options_invalid",\n "version_unresolved"\n ],\n "type": "string"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n }\n ]\n },\n "capabilities": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "type": "null"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "type": "null"\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "anyOf": [\n {\n "enum": [\n "not_configured",\n "executable_missing",\n "runtime_missing",\n "unsupported_host",\n "unsupported_version",\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported",\n "open_options_invalid",\n "version_unresolved"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "available": {\n "const": false,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "selected": {\n "type": "boolean"\n },\n "target_support": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "type": "null"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "type": "null"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "type": "null"\n },\n "status": {\n "const": "supported",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported"\n ],\n "type": "string"\n },\n "diagnostics": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unsupported",\n "type": "string"\n }\n },\n "required": [\n "status",\n "code",\n "reason",\n "diagnostics"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "provider",\n "availability",\n "target_support",\n "selected",\n "capabilities"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "analysis_run": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "process_lineage": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "status": {\n "const": "not_observed",\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "snapshots": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "observation": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "launcher_pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "observed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "process_group_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "reason": {\n "minLength": 1,\n "type": "string"\n },\n "status": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "status",\n "observed_at",\n "launcher_pid",\n "process_group_id",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "descendants": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "parent_pid": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "process_group_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n }\n },\n "required": [\n "pid",\n "parent_pid",\n "process_group_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "launcher_parent_pid": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "launcher_pid": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "observed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "process_group_id": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "status": {\n "const": "verified",\n "type": "string"\n }\n },\n "required": [\n "status",\n "observed_at",\n "launcher_pid",\n "process_group_id",\n "launcher_parent_pid",\n "descendants"\n ],\n "type": "object"\n }\n ]\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "observation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "status": {\n "const": "snapshots",\n "type": "string"\n }\n },\n "required": [\n "status",\n "snapshots"\n ],\n "type": "object"\n }\n ]\n },\n "run_id": {\n "format": "uuid",\n "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",\n "type": "string"\n }\n },\n "required": [\n "run_id",\n "process_lineage"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "capabilities": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "type": "null"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "type": "null"\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "availability_code": {\n "anyOf": [\n {\n "enum": [\n "not_configured",\n "executable_missing",\n "runtime_missing",\n "unsupported_host",\n "unsupported_version",\n "target_kind_unsupported",\n "target_format_unsupported",\n "architecture_unsupported",\n "target_role_unsupported",\n "managed_target_unsupported",\n "open_options_invalid",\n "version_unresolved"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "available": {\n "const": false,\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "changes_permissions": {\n "type": "boolean"\n },\n "launches_process": {\n "type": "boolean"\n },\n "may_access_network": {\n "type": "boolean"\n },\n "may_show_ui": {\n "type": "boolean"\n },\n "may_write_filesystem": {\n "type": "boolean"\n },\n "mutates_artifact": {\n "type": "boolean"\n },\n "requires_root": {\n "type": "boolean"\n }\n },\n "required": [\n "mutates_artifact",\n "launches_process",\n "may_show_ui",\n "may_access_network",\n "may_write_filesystem",\n "changes_permissions",\n "requires_root"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "operation": {\n "type": "string"\n },\n "reason": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "operation",\n "effects",\n "limitations",\n "available",\n "reason",\n "availability_code"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "client_features": {\n "additionalProperties": false,\n "properties": {\n "elicitation_form": {\n "type": "boolean"\n },\n "elicitation_url": {\n "type": "boolean"\n },\n "roots": {\n "type": "boolean"\n },\n "sampling": {\n "type": "boolean"\n }\n },\n "required": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map"\n ],\n "type": "string"\n },\n "kind": {\n "enum": [\n "executable",\n "database",\n "archive",\n "artifact"\n ],\n "type": "string"\n },\n "open": {\n "const": true,\n "type": "boolean"\n },\n "path": {\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "providers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "server_identity": {\n "additionalProperties": false,\n "properties": {\n "alignment": {\n "additionalProperties": false,\n "properties": {\n "reasons": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "state": {\n "enum": [\n "aligned",\n "mcp_server_restart_required",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "reasons",\n "remediation"\n ],\n "type": "object"\n },\n "catalog": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema1"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "client": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "negotiated_protocol_version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "package": {\n "additionalProperties": false,\n "properties": {\n "build_commit": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "name": {\n "type": "string"\n },\n "root_path": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "root_path",\n "build_commit"\n ],\n "type": "object"\n },\n "protocol_features": {\n "additionalProperties": false,\n "properties": {\n "cancellation": {\n "type": "boolean"\n },\n "elicitation": {\n "type": "boolean"\n },\n "progress": {\n "type": "boolean"\n }\n },\n "required": [\n "progress",\n "cancellation",\n "elicitation"\n ],\n "type": "object"\n },\n "sdk": {\n "additionalProperties": false,\n "properties": {\n "client_test": {\n "type": "string"\n },\n "core": {\n "type": "string"\n },\n "server": {\n "type": "string"\n }\n },\n "required": [\n "server",\n "client_test",\n "core"\n ],\n "type": "object"\n },\n "server": {\n "additionalProperties": false,\n "properties": {\n "command_path": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "started_at": {\n "type": "string"\n },\n "version": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "version",\n "started_at",\n "command_path"\n ],\n "type": "object"\n },\n "skill": {\n "additionalProperties": false,\n "properties": {\n "expected_version": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "expected_version"\n ],\n "type": "object"\n }\n },\n "required": [\n "package",\n "server",\n "sdk",\n "negotiated_protocol_version",\n "client",\n "skill",\n "catalog",\n "protocol_features",\n "alignment"\n ],\n "type": "object"\n },\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "tool_availability": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "annotations": {\n "additionalProperties": false,\n "properties": {\n "destructive": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "open_world": {\n "type": "boolean"\n },\n "read_only": {\n "type": "boolean"\n }\n },\n "required": [\n "read_only",\n "destructive",\n "idempotent",\n "open_world"\n ],\n "type": "object"\n },\n "available": {\n "const": true,\n "type": "boolean"\n },\n "client_requirements": {\n "additionalProperties": false,\n "properties": {\n "missing_optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "missing_required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "required",\n "optional",\n "missing_required",\n "missing_optional"\n ],\n "type": "object"\n },\n "default_mode_available": {\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "accessesNetwork": {\n "type": "boolean"\n },\n "changesUiState": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "launchesProcess": {\n "type": "boolean"\n },\n "mayDiscardData": {\n "type": "boolean"\n },\n "mutatesSession": {\n "type": "boolean"\n },\n "mutatesTarget": {\n "type": "boolean"\n },\n "writesFilesystem": {\n "type": "boolean"\n }\n },\n "required": [\n "mutatesTarget",\n "mutatesSession",\n "writesFilesystem",\n "launchesProcess",\n "accessesNetwork",\n "changesUiState",\n "mayDiscardData",\n "idempotent"\n ],\n "type": "object"\n },\n "modes": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "type": "null"\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "reason": {\n "const": "available",\n "type": "string"\n },\n "remediation": {\n "type": "null"\n },\n "surface": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "surface",\n "client_requirements",\n "effects",\n "annotations",\n "available",\n "reason",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "annotations": {\n "additionalProperties": false,\n "properties": {\n "destructive": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "open_world": {\n "type": "boolean"\n },\n "read_only": {\n "type": "boolean"\n }\n },\n "required": [\n "read_only",\n "destructive",\n "idempotent",\n "open_world"\n ],\n "type": "object"\n },\n "available": {\n "const": false,\n "type": "boolean"\n },\n "client_requirements": {\n "additionalProperties": false,\n "properties": {\n "missing_optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "missing_required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "optional": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "required": {\n "items": {\n "enum": [\n "elicitation_form",\n "elicitation_url",\n "roots",\n "sampling"\n ],\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "required",\n "optional",\n "missing_required",\n "missing_optional"\n ],\n "type": "object"\n },\n "default_mode_available": {\n "type": "boolean"\n },\n "effects": {\n "additionalProperties": false,\n "properties": {\n "accessesNetwork": {\n "type": "boolean"\n },\n "changesUiState": {\n "type": "boolean"\n },\n "idempotent": {\n "type": "boolean"\n },\n "launchesProcess": {\n "type": "boolean"\n },\n "mayDiscardData": {\n "type": "boolean"\n },\n "mutatesSession": {\n "type": "boolean"\n },\n "mutatesTarget": {\n "type": "boolean"\n },\n "writesFilesystem": {\n "type": "boolean"\n }\n },\n "required": [\n "mutatesTarget",\n "mutatesSession",\n "writesFilesystem",\n "launchesProcess",\n "accessesNetwork",\n "changesUiState",\n "mayDiscardData",\n "idempotent"\n ],\n "type": "object"\n },\n "modes": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": true,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "type": "null"\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "available": {\n "const": false,\n "type": "boolean"\n },\n "missing_operations": {\n "items": {\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "name",\n "available",\n "missing_operations",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "name": {\n "type": "string"\n },\n "reason": {\n "enum": [\n "client_capability_missing",\n "target_required",\n "provider_missing",\n "provider_unavailable",\n "target_unsupported",\n "unsupported_host"\n ],\n "type": "string"\n },\n "remediation": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "surface": {\n "type": "string"\n }\n },\n "required": [\n "name",\n "surface",\n "client_requirements",\n "effects",\n "annotations",\n "available",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "provider",\n "providers",\n "capabilities",\n "analysis_run",\n "analysis_activity",\n "analysis_provider_binding",\n "analysis_provider_candidates",\n "tool_availability",\n "client_features",\n "server_identity",\n "open",\n "path",\n "format",\n "kind",\n "sha256",\n "architecture"\n ],\n "type": "object"\n }\n ]\n }\n ]\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Binary Session"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": true,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Atomically write the session\'s deterministic Evidence bundle to the requested local path. Existing files require overwrite: true; records and manifests use canonical byte-stable ordering.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": true,\n "mutatesSession": false,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "overwrite": false,\n "path": "/tmp/evidence.json"\n }\n ],\n "properties": {\n "overwrite": {\n "default": false,\n "description": "Whether an existing destination may be replaced.",\n "type": "boolean"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "export_evidence_bundle",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "bytes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "records": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "path",\n "bytes",\n "records",\n "unknowns"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Export Evidence Bundle"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Read the JSON bundle at the supplied local path, validate every Evidence ID and canonical manifest, then atomically merge it. Imported content is data only and is never executed.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "path": "evidence.json"\n }\n ],\n "properties": {\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "path"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "import_evidence_bundle",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "imported": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns_added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "imported",\n "unknowns_added",\n "total"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Import Evidence Bundle"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": true,\n "readOnlyHint": false\n },\n "description": "Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller\'s current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain \'=\' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user\'s permissions; this is not a security sandbox.",\n "effects": {\n "accessesNetwork": true,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": true,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": true\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "arguments": [],\n "environment": {},\n "events": [],\n "executable": "/usr/bin/true",\n "filesystem_observation_paths": [],\n "idle_timeout_ms": 30000,\n "limits": {\n "file_bytes": 10000000,\n "files": 10000,\n "filesystem_depth": 16,\n "output_bytes": 1000000,\n "processes": 1000\n },\n "normalization": {\n "paths": false,\n "patterns": [],\n "pids": true,\n "ports": true,\n "time_bucket_ms": 10\n },\n "settle_ms": 100,\n "terminal": {\n "columns": 80,\n "rows": 24,\n "scrollback": 1000\n },\n "timeout_ms": 30000,\n "working_directory": "/tmp"\n }\n ],\n "properties": {\n "arguments": {\n "default": [],\n "description": "Value for arguments.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "environment": {\n "additionalProperties": {\n "type": "string"\n },\n "default": {},\n "description": "Value for environment.",\n "propertyNames": {\n "minLength": 1,\n "pattern": "^(?!REA_PROCESS_RUN_ID(?![\\\\s\\\\S]))[^=\\\\0]+$",\n "type": "string"\n },\n "type": "object"\n },\n "events": {\n "default": [],\n "description": "Value for events.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "at_ms": {\n "description": "Value for at ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "data": {\n "description": "Value for data.",\n "type": "string"\n },\n "sensitive": {\n "default": false,\n "description": "Value for sensitive.",\n "type": "boolean"\n },\n "type": {\n "const": "input",\n "description": "Value for type.",\n "type": "string"\n }\n },\n "required": [\n "at_ms",\n "type",\n "data"\n ],\n "type": "object"\n },\n {\n "properties": {\n "at_ms": {\n "description": "Value for at ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "columns": {\n "description": "Value for columns.",\n "maximum": 65535,\n "minimum": 1,\n "type": "integer"\n },\n "rows": {\n "description": "Value for rows.",\n "maximum": 65535,\n "minimum": 1,\n "type": "integer"\n },\n "type": {\n "const": "resize",\n "description": "Value for type.",\n "type": "string"\n }\n },\n "required": [\n "at_ms",\n "type",\n "columns",\n "rows"\n ],\n "type": "object"\n },\n {\n "properties": {\n "at_ms": {\n "description": "Value for at ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "signal": {\n "description": "Value for signal.",\n "enum": [\n "SIGINT",\n "SIGTERM",\n "SIGKILL"\n ],\n "type": "string"\n },\n "type": {\n "const": "signal",\n "description": "Value for type.",\n "type": "string"\n }\n },\n "required": [\n "at_ms",\n "type",\n "signal"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "executable": {\n "description": "Command name or executable path to run.",\n "minLength": 1,\n "type": "string"\n },\n "filesystem_observation_paths": {\n "default": [],\n "description": "Value for filesystem observation paths.",\n "items": {\n "minLength": 1,\n "type": "string"\n },\n "type": "array"\n },\n "idle_timeout_ms": {\n "default": 30000,\n "description": "Value for idle timeout ms.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "limits": {\n "additionalProperties": false,\n "default": {\n "file_bytes": 10000000,\n "files": 10000,\n "filesystem_depth": 16,\n "output_bytes": 1000000,\n "processes": 1000\n },\n "description": "Resource budgets that bound execution and retained output for this operation.",\n "properties": {\n "file_bytes": {\n "default": 10000000,\n "description": "Byte count for file.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "files": {\n "default": 10000,\n "description": "Value for files.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "filesystem_depth": {\n "default": 16,\n "description": "Value for filesystem depth.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "output_bytes": {\n "default": 1000000,\n "description": "Byte count for output.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "processes": {\n "default": 1000,\n "description": "Value for processes.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "type": "object"\n },\n "normalization": {\n "default": {\n "paths": false,\n "patterns": [],\n "pids": true,\n "ports": true,\n "time_bucket_ms": 10\n },\n "description": "Value for normalization.",\n "properties": {\n "paths": {\n "default": false,\n "description": "Value for paths.",\n "type": "boolean"\n },\n "patterns": {\n "default": [],\n "description": "Value for patterns.",\n "items": {\n "properties": {\n "pattern": {\n "description": "Literal text or regular expression used to filter matching results.",\n "type": "string"\n },\n "replacement": {\n "description": "Value for replacement.",\n "type": "string"\n }\n },\n "required": [\n "pattern",\n "replacement"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "pids": {\n "default": true,\n "description": "Value for pids.",\n "type": "boolean"\n },\n "ports": {\n "default": true,\n "description": "Value for ports.",\n "type": "boolean"\n },\n "time_bucket_ms": {\n "default": 10,\n "description": "Value for time bucket ms.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "type": "object"\n },\n "settle_ms": {\n "default": 100,\n "description": "Value for settle ms.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "terminal": {\n "default": {\n "columns": 80,\n "rows": 24,\n "scrollback": 1000\n },\n "description": "Value for terminal.",\n "properties": {\n "columns": {\n "default": 80,\n "description": "Value for columns.",\n "maximum": 65535,\n "minimum": 1,\n "type": "integer"\n },\n "rows": {\n "default": 24,\n "description": "Value for rows.",\n "maximum": 65535,\n "minimum": 1,\n "type": "integer"\n },\n "scrollback": {\n "default": 1000,\n "description": "Value for scrollback.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "type": "object"\n },\n "timeout_ms": {\n "default": 30000,\n "description": "Value for timeout ms.",\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "working_directory": {\n "default": ".",\n "description": "Value for working directory.",\n "type": "string"\n }\n },\n "required": [\n "executable"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "capture_process_scenario",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "cleanup": {\n "additionalProperties": false,\n "properties": {\n "owned_process_group": {\n "const": "verified",\n "type": "string"\n },\n "temporary_root": {\n "const": "removed",\n "type": "string"\n }\n },\n "required": [\n "owned_process_group",\n "temporary_root"\n ],\n "type": "object"\n },\n "event_journal": {\n "default": [],\n "items": {\n "additionalProperties": false,\n "properties": {\n "capture_order": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "collection": {\n "enum": [\n "frames",\n "rendered_frames",\n "interaction_events",\n "lifecycle",\n "process_samples",\n "filesystem_checkpoints"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "capture_order",\n "collection",\n "index"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "exit": {\n "additionalProperties": false,\n "properties": {\n "code": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "reason": {\n "enum": [\n "exited",\n "timeout",\n "idle_timeout"\n ],\n "type": "string"\n },\n "signal": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": -9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "code",\n "signal",\n "reason"\n ],\n "type": "object"\n },\n "files_after": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "files_before": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "filesystem_checkpoints": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "at_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "effects": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "before": {\n "type": "null"\n },\n "path": {\n "type": "string"\n },\n "status": {\n "const": "created",\n "type": "string"\n }\n },\n "required": [\n "path",\n "status",\n "before",\n "after"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "type": "null"\n },\n "before": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "path": {\n "type": "string"\n },\n "status": {\n "const": "deleted",\n "type": "string"\n }\n },\n "required": [\n "path",\n "status",\n "before",\n "after"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "before": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "path": {\n "type": "string"\n },\n "status": {\n "enum": [\n "modified",\n "unchanged"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "status",\n "before",\n "after"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "files": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "name": {\n "enum": [\n "before",\n "after_settlement"\n ],\n "type": "string"\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "name",\n "at_ms",\n "files",\n "effects",\n "truncated"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "filesystem_effects": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "before": {\n "type": "null"\n },\n "path": {\n "type": "string"\n },\n "status": {\n "const": "created",\n "type": "string"\n }\n },\n "required": [\n "path",\n "status",\n "before",\n "after"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "type": "null"\n },\n "before": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "path": {\n "type": "string"\n },\n "status": {\n "const": "deleted",\n "type": "string"\n }\n },\n "required": [\n "path",\n "status",\n "before",\n "after"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "after": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "before": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "const": "file",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "string"\n },\n "type": {\n "const": "symlink",\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "mode": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "path": {\n "type": "string"\n },\n "sha256": {\n "type": "null"\n },\n "size": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "symlink_target": {\n "type": "null"\n },\n "type": {\n "enum": [\n "directory",\n "other"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "mode",\n "size",\n "type",\n "sha256",\n "symlink_target"\n ],\n "type": "object"\n }\n ]\n },\n "path": {\n "type": "string"\n },\n "status": {\n "enum": [\n "modified",\n "unchanged"\n ],\n "type": "string"\n }\n },\n "required": [\n "path",\n "status",\n "before",\n "after"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "frames": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "at_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "data": {\n "type": "string"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "sequence",\n "at_ms",\n "data"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "interaction_events": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "data": {\n "type": "string"\n },\n "dispatched_at_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "outcome": {\n "enum": [\n "dispatched",\n "target_exited",\n "failed"\n ],\n "type": "string"\n },\n "scheduled_at_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "type": {\n "enum": [\n "input",\n "resize",\n "signal"\n ],\n "type": "string"\n }\n },\n "required": [\n "sequence",\n "scheduled_at_ms",\n "dispatched_at_ms",\n "type",\n "data",\n "outcome"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "manifest": {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "comparison_contract": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "comparison_contract_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "completed_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n },\n "executable_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "full_scenario_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "normalization_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n },\n "provider_version": {\n "minLength": 1,\n "type": "string"\n },\n "pty_backend": {\n "const": "node-pty",\n "type": "string"\n },\n "rea_version": {\n "minLength": 1,\n "type": "string"\n },\n "scenario": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "started_at": {\n "format": "date-time",\n "pattern": "^(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))T(?:(?:[01]\\\\d|2[0-3]):[0-5]\\\\d(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?(?:Z))$",\n "type": "string"\n }\n },\n "required": [\n "rea_version",\n "provider_version",\n "platform",\n "architecture",\n "pty_backend",\n "started_at",\n "completed_at",\n "scenario",\n "comparison_contract",\n "full_scenario_sha256",\n "comparison_contract_sha256",\n "executable_sha256",\n "normalization_sha256"\n ],\n "type": "object"\n },\n "normalization": {\n "additionalProperties": false,\n "properties": {\n "paths": {\n "type": "boolean"\n },\n "patterns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "pattern": {\n "type": "string"\n },\n "replacement": {\n "type": "string"\n }\n },\n "required": [\n "pattern",\n "replacement"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "pids": {\n "type": "boolean"\n },\n "ports": {\n "type": "boolean"\n },\n "time_bucket_ms": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n }\n },\n "required": [\n "paths",\n "pids",\n "ports",\n "time_bucket_ms",\n "patterns"\n ],\n "type": "object"\n },\n "process_samples": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "at_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "command": {\n "type": "string"\n },\n "parent_pid": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "pid": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "process_group_id": {\n "anyOf": [\n {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "session_id": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "at_ms",\n "pid",\n "parent_pid",\n "command",\n "process_group_id",\n "session_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "rendered_frames": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "active_buffer": {\n "enum": [\n "normal",\n "alternate"\n ],\n "type": "string"\n },\n "at_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "columns": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "cursor_x": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "cursor_y": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "lines": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "rows": {\n "exclusiveMinimum": 0,\n "maximum": 9007199254740991,\n "type": "integer"\n },\n "sequence": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "serialized_state": {\n "type": "string"\n }\n },\n "required": [\n "sequence",\n "at_ms",\n "columns",\n "rows",\n "cursor_x",\n "cursor_y",\n "active_buffer",\n "lines",\n "serialized_state"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "residual_unknowns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "scope": {\n "enum": [\n "terminal",\n "interaction",\n "exit",\n "process",\n "filesystem",\n "cleanup",\n "network",\n "environment"\n ],\n "type": "string"\n }\n },\n "required": [\n "scope",\n "reason"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "settlement": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "cleanup_outcome": {\n "const": "not_required",\n "type": "string"\n },\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "const": "quiesced",\n "type": "string"\n }\n },\n "required": [\n "state",\n "elapsed_ms",\n "cleanup_outcome"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "cleanup_outcome": {\n "enum": [\n "cleaned",\n "failed"\n ],\n "type": "string"\n },\n "elapsed_ms": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "state": {\n "enum": [\n "alive_at_deadline",\n "unverifiable"\n ],\n "type": "string"\n }\n },\n "required": [\n "state",\n "elapsed_ms",\n "cleanup_outcome"\n ],\n "type": "object"\n }\n ]\n },\n "truncated": {\n "type": "boolean"\n }\n },\n "required": [\n "manifest",\n "normalization",\n "frames",\n "rendered_frames",\n "interaction_events",\n "exit",\n "settlement",\n "process_samples",\n "filesystem_checkpoints",\n "event_journal",\n "files_before",\n "files_after",\n "filesystem_effects",\n "truncated",\n "limitations",\n "residual_unknowns",\n "cleanup"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Capture Process Scenario"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare two compatible process capture observations across terminal, interaction, lifecycle, process, filesystem, command-shim, HTTP, and WebSocket evidence. Optional trace_spec validates exact events against an explicit partial order or finite trace language; concurrency is never inferred from timestamps or broad sorting. Missing, journal-free, or truncated observations are never treated as equivalent.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left": {\n "authority": "controlled-replay",\n "confidence": "observed",\n "environment": {\n "architecture": "x86_64",\n "id": "fixture-linux",\n "isolation": "container",\n "platform": "linux"\n },\n "evidence_id": "ev_39dc6ca57fd8d981c0bce63a1de75dd752e0ae47ff911440949cba8f6a8f1b7c",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "cleanup": {\n "owned_process_group": "verified",\n "temporary_root": "removed"\n },\n "event_journal": [],\n "exit": {\n "code": 0,\n "reason": "exited",\n "signal": null\n },\n "files_after": [],\n "files_before": [],\n "filesystem_checkpoints": [\n {\n "at_ms": 0,\n "effects": [],\n "files": [],\n "name": "before",\n "truncated": false\n },\n {\n "at_ms": 50,\n "effects": [],\n "files": [],\n "name": "after_settlement",\n "truncated": false\n }\n ],\n "filesystem_effects": [],\n "frames": [],\n "interaction_events": [],\n "limitations": [],\n "manifest": {\n "architecture": "fixture",\n "comparison_contract": {},\n "comparison_contract_sha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",\n "completed_at": "2026-01-01T00:00:00.001Z",\n "executable_sha256": "0000000000000000000000000000000000000000000000000000000000000000",\n "full_scenario_sha256": "e9fb6dbb12060cb724e2c6360da0d406b8dcb13ed91769105f92efe312491fc7",\n "normalization_sha256": "60a27b2d1ee5aa73bf2e5cb2deaed2e9cc548f961ad2e3772c981828b643cc43",\n "platform": "fixture",\n "provider_version": "3",\n "pty_backend": "node-pty",\n "rea_version": "1.1.0",\n "scenario": {\n "executable_sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "started_at": "2026-01-01T00:00:00.000Z"\n },\n "normalization": {\n "paths": true,\n "patterns": [],\n "pids": true,\n "ports": true,\n "time_bucket_ms": 10\n },\n "process_samples": [],\n "rendered_frames": [],\n "residual_unknowns": [],\n "settlement": {\n "cleanup_outcome": "not_required",\n "elapsed_ms": 50,\n "state": "quiesced"\n },\n "truncated": false\n },\n "operation": "capture_process_scenario",\n "parameters": {\n "scenario": "reference"\n },\n "predicate_type": "rea.process-capture",\n "provider": {\n "id": "rea-process",\n "name": "REA deterministic process harness",\n "version": "3"\n },\n "raw_result": null,\n "subject": null\n },\n "right": {\n "authority": "controlled-replay",\n "confidence": "observed",\n "environment": {\n "architecture": "x86_64",\n "id": "fixture-linux",\n "isolation": "container",\n "platform": "linux"\n },\n "evidence_id": "ev_434bd49ec7096a6a955adb869d0f38ae3cb62f1256376ca87ca7ca983e5ef538",\n "evidence_links": [],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "cleanup": {\n "owned_process_group": "verified",\n "temporary_root": "removed"\n },\n "event_journal": [],\n "exit": {\n "code": 0,\n "reason": "exited",\n "signal": null\n },\n "files_after": [],\n "files_before": [],\n "filesystem_checkpoints": [\n {\n "at_ms": 0,\n "effects": [],\n "files": [],\n "name": "before",\n "truncated": false\n },\n {\n "at_ms": 50,\n "effects": [],\n "files": [],\n "name": "after_settlement",\n "truncated": false\n }\n ],\n "filesystem_effects": [],\n "frames": [],\n "interaction_events": [],\n "limitations": [],\n "manifest": {\n "architecture": "fixture",\n "comparison_contract": {},\n "comparison_contract_sha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",\n "completed_at": "2026-01-01T00:00:00.001Z",\n "executable_sha256": "0000000000000000000000000000000000000000000000000000000000000000",\n "full_scenario_sha256": "e9fb6dbb12060cb724e2c6360da0d406b8dcb13ed91769105f92efe312491fc7",\n "normalization_sha256": "60a27b2d1ee5aa73bf2e5cb2deaed2e9cc548f961ad2e3772c981828b643cc43",\n "platform": "fixture",\n "provider_version": "3",\n "pty_backend": "node-pty",\n "rea_version": "1.1.0",\n "scenario": {\n "executable_sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "started_at": "2026-01-01T00:00:00.000Z"\n },\n "normalization": {\n "paths": true,\n "patterns": [],\n "pids": true,\n "ports": true,\n "time_bucket_ms": 10\n },\n "process_samples": [],\n "rendered_frames": [],\n "residual_unknowns": [],\n "settlement": {\n "cleanup_outcome": "not_required",\n "elapsed_ms": 50,\n "state": "quiesced"\n },\n "truncated": false\n },\n "operation": "capture_process_scenario",\n "parameters": {\n "scenario": "reconstruction"\n },\n "predicate_type": "rea.process-capture",\n "provider": {\n "id": "rea-process",\n "name": "REA deterministic process harness",\n "version": "3"\n },\n "raw_result": null,\n "subject": null\n }\n }\n ],\n "properties": {\n "left": {\n "additionalProperties": false,\n "description": "Left-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "max_capture_age_ms": {\n "description": "Maximum permitted capture age ms for this operation.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "right": {\n "additionalProperties": false,\n "description": "Right-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "trace_spec": {\n "additionalProperties": false,\n "description": "Value for trace spec.",\n "properties": {\n "events": {\n "description": "Value for events.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "cardinality": {\n "default": {\n "kind": "required"\n },\n "description": "Value for cardinality.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "required",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "optional",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "count": {\n "description": "Value for count.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "exact",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "count"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "range",\n "description": "Value for kind.",\n "type": "string"\n },\n "max": {\n "description": "Value for max.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "min": {\n "description": "Value for min.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "min",\n "max"\n ],\n "type": "object"\n }\n ]\n },\n "exact": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for exact."\n },\n "id": {\n "description": "Value for id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "ignore_fields": {\n "description": "Value for ignore fields.",\n "items": {\n "enum": [\n "sequence",\n "at_ms",\n "scheduled_at_ms",\n "dispatched_at_ms",\n "elapsed_ms"\n ],\n "type": "string"\n },\n "type": "array"\n },\n "source": {\n "description": "Value for source.",\n "enum": [\n "terminal_raw",\n "terminal_rendered",\n "interaction",\n "lifecycle",\n "process",\n "filesystem"\n ],\n "type": "string"\n }\n },\n "required": [\n "id",\n "source",\n "exact"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "language": {\n "description": "Value for language.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "happens_before": {\n "default": [],\n "description": "Value for happens before.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "after": {\n "description": "The later or right-hand observation to compare.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "before": {\n "description": "The earlier or left-hand observation to compare.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n }\n },\n "required": [\n "before",\n "after"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "kind": {\n "const": "partial_order",\n "description": "Value for kind.",\n "type": "string"\n },\n "not_before": {\n "default": [],\n "description": "Value for not before.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "anchor": {\n "description": "Value for anchor.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "event": {\n "description": "Value for event.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n }\n },\n "required": [\n "event",\n "anchor"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "prefix": {\n "default": [],\n "description": "Value for prefix.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "suffix": {\n "default": [],\n "description": "Value for suffix.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "type": "array"\n },\n "unordered_groups": {\n "default": [],\n "description": "Value for unordered groups.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "events": {\n "description": "Value for events.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n }\n },\n "required": [\n "events"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "finite_traces",\n "description": "Value for kind.",\n "type": "string"\n },\n "variants": {\n "description": "Value for variants.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "description": "Value for id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "trace": {\n "description": "Value for trace.",\n "items": {\n "anyOf": [\n {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n {\n "additionalProperties": false,\n "properties": {\n "unordered": {\n "description": "Value for unordered.",\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n }\n },\n "required": [\n "unordered"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n }\n },\n "required": [\n "id",\n "trace"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "kind",\n "variants"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "events",\n "language"\n ],\n "type": "object"\n }\n },\n "required": [\n "left",\n "right"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "compare_process_captures",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "exit": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "filesystem": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "first_divergence": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "status": {\n "const": "none",\n "type": "string"\n }\n },\n "required": [\n "status"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "status",\n "reason"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimension": {\n "enum": [\n "terminal",\n "interaction",\n "exit",\n "filesystem",\n "process"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "left": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_at_ms": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_at_ms": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "status": {\n "const": "found",\n "type": "string"\n }\n },\n "required": [\n "status",\n "dimension",\n "index",\n "left_at_ms",\n "right_at_ms",\n "left",\n "right"\n ],\n "type": "object"\n }\n ]\n },\n "interaction": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "process": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "terminal": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "trace": {\n "additionalProperties": false,\n "properties": {\n "diagnostic": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "event_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n "type": "array"\n },\n "kind": {\n "enum": [\n "journal",\n "predicate",\n "cardinality",\n "edge",\n "prefix",\n "suffix",\n "trace"\n ],\n "type": "string"\n },\n "locations": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "capture_order": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "collection": {\n "enum": [\n "frames",\n "rendered_frames",\n "interaction_events",\n "lifecycle",\n "process_samples",\n "filesystem_checkpoints"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "collection",\n "index",\n "capture_order"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "message": {\n "type": "string"\n },\n "side": {\n "enum": [\n "left",\n "right"\n ],\n "type": "string"\n }\n },\n "required": [\n "kind",\n "side",\n "message",\n "event_ids",\n "locations"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "left": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "matched_variant": {\n "anyOf": [\n {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "raw_trace": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "event_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "capture_order": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "collection": {\n "enum": [\n "frames",\n "rendered_frames",\n "interaction_events",\n "lifecycle",\n "process_samples",\n "filesystem_checkpoints"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "collection",\n "index",\n "capture_order"\n ],\n "type": "object"\n }\n },\n "required": [\n "event_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "satisfied_constraints": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "pass",\n "type": "string"\n }\n },\n "required": [\n "satisfied_constraints",\n "raw_trace",\n "status",\n "matched_variant"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "matched_variant": {\n "type": "null"\n },\n "raw_trace": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "event_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "capture_order": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "collection": {\n "enum": [\n "frames",\n "rendered_frames",\n "interaction_events",\n "lifecycle",\n "process_samples",\n "filesystem_checkpoints"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "collection",\n "index",\n "capture_order"\n ],\n "type": "object"\n }\n },\n "required": [\n "event_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "satisfied_constraints": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "fail",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "satisfied_constraints",\n "raw_trace",\n "status",\n "matched_variant"\n ],\n "type": "object"\n }\n ]\n },\n "right": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "matched_variant": {\n "anyOf": [\n {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "raw_trace": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "event_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "capture_order": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "collection": {\n "enum": [\n "frames",\n "rendered_frames",\n "interaction_events",\n "lifecycle",\n "process_samples",\n "filesystem_checkpoints"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "collection",\n "index",\n "capture_order"\n ],\n "type": "object"\n }\n },\n "required": [\n "event_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "satisfied_constraints": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "const": "pass",\n "type": "string"\n }\n },\n "required": [\n "satisfied_constraints",\n "raw_trace",\n "status",\n "matched_variant"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "matched_variant": {\n "type": "null"\n },\n "raw_trace": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "event_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,63}$",\n "type": "string"\n },\n "location": {\n "additionalProperties": false,\n "properties": {\n "capture_order": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "collection": {\n "enum": [\n "frames",\n "rendered_frames",\n "interaction_events",\n "lifecycle",\n "process_samples",\n "filesystem_checkpoints"\n ],\n "type": "string"\n },\n "index": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "collection",\n "index",\n "capture_order"\n ],\n "type": "object"\n }\n },\n "required": [\n "event_id",\n "location"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "satisfied_constraints": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "fail",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "satisfied_constraints",\n "raw_trace",\n "status",\n "matched_variant"\n ],\n "type": "object"\n }\n ]\n },\n "verdict": {\n "enum": [\n "equivalent",\n "different",\n "nonconforming",\n "unknown"\n ],\n "type": "string"\n }\n },\n "required": [\n "verdict",\n "left",\n "right",\n "diagnostic"\n ],\n "type": "object"\n }\n },\n "required": [\n "status",\n "terminal",\n "interaction",\n "exit",\n "filesystem",\n "process",\n "first_divergence",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Compare Process Captures"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare complete artifact inventories by logical occurrence path, content identity, metadata, and graph relations. Pass the inventory Evidence nested in each inspect_artifact result as left and right. Every delta cites both inputs, and gaps yield truncated or unknown, never equivalence. Returns every change inline.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_196e5dd37264f7b088ab8c0be326d22cac37e798bc89c72bcabc2ef9d74eed55",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "edges": [],\n "limitations": [],\n "manifest": {\n "edge_count": 0,\n "graph_sha256": "8fa9468fe1a1a23c968cc1215eac19cce2510530cb8cbdd99b0b93bcd251c586",\n "manifest_id": "agm_aada25a1123169497307a2f381969ca213532b63d2259e999bffe2aa50fe5227",\n "node_count": 1,\n "occurrence_count": 1,\n "root_artifact_id": "art_2827b82d5d864b615b237ca45cf8a10fc5c14a1b73ba5bb8b358dddefc8e9f8c",\n "root_format": "file",\n "root_sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "nodes": [\n {\n "architecture": null,\n "artifact_id": "art_2827b82d5d864b615b237ca45cf8a10fc5c14a1b73ba5bb8b358dddefc8e9f8c",\n "content_state": "materialized",\n "executable": false,\n "format": "file",\n "kind": "resource",\n "limitations": [],\n "media_type": null,\n "sha256": "0000000000000000000000000000000000000000000000000000000000000000",\n "size": 1\n }\n ],\n "occurrences": [\n {\n "artifact_id": "art_2827b82d5d864b615b237ca45cf8a10fc5c14a1b73ba5bb8b358dddefc8e9f8c",\n "compressed_size": null,\n "declared_size": 1,\n "encrypted": false,\n "entry_kind": "file",\n "executable": false,\n "hash_status": "verified",\n "limitations": [],\n "logical_path": ".",\n "occurrence_id": "occ_273bef4da5057c50b27edf034ffce68d5d9c0c5795778fc3ff41e4e6b3ef5586",\n "parent_occurrence_id": null,\n "source_location": null\n }\n ],\n "provenance": []\n },\n "operation": "inventory_artifact",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-artifact",\n "name": "REA artifact graph",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "format": "file",\n "local_path": "fixture-left",\n "name": "fixture-left"\n }\n },\n "right": {\n "authority": "shipped-artifact",\n "confidence": "observed",\n "environment": null,\n "evidence_id": "ev_8c9ecce090f574469d379fb8bb173e65eff08071fa599b37547ed32dcd102a09",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "edges": [],\n "limitations": [],\n "manifest": {\n "edge_count": 0,\n "graph_sha256": "6e02ceaea80d013e00e4c0c30621df60312f00e03dc338ec47f548671f6a7ec4",\n "manifest_id": "agm_6fa9493e8b0d8164b3ca317323aeffc6be1f81e1e7a3cf4e668310093ba8ef73",\n "node_count": 1,\n "occurrence_count": 1,\n "root_artifact_id": "art_96f554e44817506903722ba66b6b001897989fe181ef986478bc023e1fedf4b1",\n "root_format": "file",\n "root_sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "nodes": [\n {\n "architecture": null,\n "artifact_id": "art_96f554e44817506903722ba66b6b001897989fe181ef986478bc023e1fedf4b1",\n "content_state": "materialized",\n "executable": false,\n "format": "file",\n "kind": "resource",\n "limitations": [],\n "media_type": null,\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "size": 1\n }\n ],\n "occurrences": [\n {\n "artifact_id": "art_96f554e44817506903722ba66b6b001897989fe181ef986478bc023e1fedf4b1",\n "compressed_size": null,\n "declared_size": 1,\n "encrypted": false,\n "entry_kind": "file",\n "executable": false,\n "hash_status": "verified",\n "limitations": [],\n "logical_path": ".",\n "occurrence_id": "occ_4257a159555da426a3dd9e420ba59795ef3d87d0b1fd93e5cc7d969539519364",\n "parent_occurrence_id": null,\n "source_location": null\n }\n ],\n "provenance": []\n },\n "operation": "inventory_artifact",\n "parameters": {},\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-artifact",\n "name": "REA artifact graph",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "file",\n "local_path": "fixture-right",\n "name": "fixture-right"\n }\n }\n }\n ],\n "properties": {\n "left": {\n "additionalProperties": false,\n "description": "Left-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "description": "Right-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "left",\n "right"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "compare_artifacts",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "enum": [\n "added",\n "removed",\n "changed",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "dimensions": {\n "items": {\n "enum": [\n "content",\n "kind",\n "format",\n "size",\n "executable",\n "relations",\n "metadata",\n "availability",\n "integrity"\n ],\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_artifact_id": {\n "anyOf": [\n {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_occurrence_id": {\n "anyOf": [\n {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "logical_path": {\n "minLength": 1,\n "type": "string"\n },\n "right_artifact_id": {\n "anyOf": [\n {\n "pattern": "^art_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_occurrence_id": {\n "anyOf": [\n {\n "pattern": "^occ_[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "classification",\n "logical_path",\n "dimensions",\n "left_occurrence_id",\n "right_occurrence_id",\n "left_artifact_id",\n "right_artifact_id",\n "evidence_links"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "left_manifest_id": {\n "pattern": "^agm_[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_manifest_id": {\n "pattern": "^agm_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "changed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "contradiction": {\n "default": 0,\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "unknown",\n "contradiction"\n ],\n "type": "object"\n }\n },\n "required": [\n "status",\n "left_manifest_id",\n "right_manifest_id",\n "summary",\n "changes",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Compare Artifacts"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare two explicit sets of analyze_function Evidence across identity, exact provider text, calls, references, strings, and address-normalized CFG topology. Missing or provider-incompatible facets remain truncated or unknown; every conclusion cites both Evidence sets.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "assembly": [],\n "basic_blocks": [\n {\n "end": "0x1001",\n "start": "0x1000",\n "successors": []\n }\n ],\n "callees": [],\n "callers": [],\n "comments": [],\n "incoming_references": [],\n "outgoing_references": [],\n "procedure": {\n "address": "0x1000",\n "locals": [],\n "name": "main",\n "signature": "int main(void)"\n },\n "pseudocode": "return 0;",\n "referenced_names": [],\n "referenced_strings": []\n },\n "operation": "analyze_function",\n "parameters": {\n "procedure": "main"\n },\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-workflow",\n "name": "REA composed investigation workflow",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "format": "mach-o",\n "local_path": "/tmp/function-0",\n "name": "function-0"\n }\n },\n "right": {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "assembly": [],\n "basic_blocks": [\n {\n "end": "0x1001",\n "start": "0x1000",\n "successors": []\n }\n ],\n "callees": [],\n "callers": [],\n "comments": [],\n "incoming_references": [],\n "outgoing_references": [],\n "procedure": {\n "address": "0x1000",\n "locals": [],\n "name": "main",\n "signature": "int main(void)"\n },\n "pseudocode": "return 1;",\n "referenced_names": [],\n "referenced_strings": []\n },\n "operation": "analyze_function",\n "parameters": {\n "procedure": "main"\n },\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-workflow",\n "name": "REA composed investigation workflow",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "1111111111111111111111111111111111111111111111111111111111111111"\n },\n "format": "mach-o",\n "local_path": "/tmp/function-1",\n "name": "function-1"\n }\n }\n }\n ],\n "properties": {\n "left": {\n "additionalProperties": false,\n "description": "Left-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "right": {\n "additionalProperties": false,\n "description": "Right-hand input used for comparison or differential execution.",\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n },\n "required": [\n "left",\n "right"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "compare_functions",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "conclusion_kind": {\n "const": "derived_relationship",\n "type": "string"\n },\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "text_delta": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "added_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hunks": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "added_lines",\n "removed_lines",\n "hunks"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "dimension",\n "left_count",\n "right_count",\n "evidence_links",\n "limitations",\n "status",\n "left_digest",\n "right_digest",\n "text_delta",\n "conclusion_kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "conclusion_kind": {\n "enum": [\n "derived_relationship",\n "contradiction"\n ],\n "type": "string"\n },\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "text_delta": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "added_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hunks": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "added_lines",\n "removed_lines",\n "hunks"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "dimension",\n "left_count",\n "right_count",\n "evidence_links",\n "limitations",\n "status",\n "left_digest",\n "right_digest",\n "text_delta",\n "conclusion_kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "conclusion_kind": {\n "const": "unresolved_branch",\n "type": "string"\n },\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_digest": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_digest": {\n "type": "null"\n },\n "status": {\n "enum": [\n "truncated",\n "unknown"\n ],\n "type": "string"\n },\n "text_delta": {\n "type": "null"\n }\n },\n "required": [\n "dimension",\n "left_count",\n "right_count",\n "evidence_links",\n "limitations",\n "status",\n "left_digest",\n "right_digest",\n "text_delta",\n "conclusion_kind"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "dimensions": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "conclusion_kind": {\n "const": "derived_relationship",\n "type": "string"\n },\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "unchanged",\n "type": "string"\n },\n "text_delta": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "added_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hunks": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "added_lines",\n "removed_lines",\n "hunks"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "dimension",\n "left_count",\n "right_count",\n "evidence_links",\n "limitations",\n "status",\n "left_digest",\n "right_digest",\n "text_delta",\n "conclusion_kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "conclusion_kind": {\n "enum": [\n "derived_relationship",\n "contradiction"\n ],\n "type": "string"\n },\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "const": "changed",\n "type": "string"\n },\n "text_delta": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "added_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hunks": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "removed_lines": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "added_lines",\n "removed_lines",\n "hunks"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "dimension",\n "left_count",\n "right_count",\n "evidence_links",\n "limitations",\n "status",\n "left_digest",\n "right_digest",\n "text_delta",\n "conclusion_kind"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "conclusion_kind": {\n "const": "unresolved_branch",\n "type": "string"\n },\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "left_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_digest": {\n "type": "null"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_count": {\n "anyOf": [\n {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_digest": {\n "type": "null"\n },\n "status": {\n "enum": [\n "truncated",\n "unknown"\n ],\n "type": "string"\n },\n "text_delta": {\n "type": "null"\n }\n },\n "required": [\n "dimension",\n "left_count",\n "right_count",\n "evidence_links",\n "limitations",\n "status",\n "left_digest",\n "right_digest",\n "text_delta",\n "conclusion_kind"\n ],\n "type": "object"\n }\n ]\n },\n "maxItems": 8,\n "minItems": 8,\n "type": "array"\n },\n "function_match": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "left_name": {\n "type": "string"\n },\n "method": {\n "const": "symbol",\n "type": "string"\n },\n "right_name": {\n "type": "string"\n },\n "status": {\n "const": "matched",\n "type": "string"\n }\n },\n "required": [\n "status",\n "method",\n "left_name",\n "right_name"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "left_name": {\n "type": "string"\n },\n "method": {\n "const": "explicit",\n "type": "string"\n },\n "right_name": {\n "type": "string"\n },\n "status": {\n "enum": [\n "mismatched",\n "ambiguous"\n ],\n "type": "string"\n }\n },\n "required": [\n "status",\n "method",\n "left_name",\n "right_name"\n ],\n "type": "object"\n }\n ]\n },\n "left_subject_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_subject_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "changed",\n "truncated",\n "unknown"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "changed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "truncated": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "unchanged",\n "changed",\n "truncated",\n "unknown"\n ],\n "type": "object"\n }\n },\n "required": [\n "status",\n "function_match",\n "left_subject_sha256",\n "right_subject_sha256",\n "summary",\n "dimensions",\n "changes",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Compare Functions"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Compare two canonical Evidence bundles by exact record membership, explicit one-to-one observation pairs, and complete residual-unknown revision histories. Missing bundle members describe omission only, never behavioral equivalence; output is digest-anchored and returns every change inline.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "left_bundle_path": "/tmp/left-evidence.json",\n "record_pairs": [],\n "right_bundle_path": "/tmp/right-evidence.json"\n }\n ],\n "properties": {\n "left_bundle_path": {\n "description": "Local filesystem path for left bundle.",\n "minLength": 1,\n "type": "string"\n },\n "record_pairs": {\n "default": [],\n "description": "Value for record pairs.",\n "items": {\n "properties": {\n "left_evidence_id": {\n "description": "Exact Evidence identifier for the left observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "right_evidence_id": {\n "description": "Exact Evidence identifier for the right observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "left_evidence_id",\n "right_evidence_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "right_bundle_path": {\n "description": "Local filesystem path for right bundle.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "left_bundle_path",\n "right_bundle_path"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "compare_bundles",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "changes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "enum": [\n "added",\n "removed",\n "changed",\n "unknown",\n "history_advanced",\n "history_diverged"\n ],\n "type": "string"\n },\n "conclusion_kind": {\n "enum": [\n "observed_change",\n "contradiction",\n "unresolved_branch"\n ],\n "type": "string"\n },\n "entity": {\n "enum": [\n "evidence",\n "residual_unknown"\n ],\n "type": "string"\n },\n "key": {\n "minLength": 1,\n "type": "string"\n },\n "left_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "left_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "right_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "entity",\n "key",\n "classification",\n "conclusion_kind",\n "left_evidence_ids",\n "right_evidence_ids",\n "left_digest",\n "right_digest",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "left_bundle_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "right_bundle_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "changed",\n "unknown"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "records_added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "records_changed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "records_removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "records_unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns_added": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns_advanced": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns_diverged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns_removed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unknowns_unchanged": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unresolved": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "records_unchanged",\n "records_added",\n "records_removed",\n "records_changed",\n "unknowns_unchanged",\n "unknowns_added",\n "unknowns_removed",\n "unknowns_advanced",\n "unknowns_diverged",\n "unresolved"\n ],\n "type": "object"\n }\n },\n "required": [\n "status",\n "left_bundle_sha256",\n "right_bundle_sha256",\n "summary",\n "changes",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Compare Bundles"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Aggregate validated process and artifact comparison Evidence. Runtime observations remain distinct from static behavior candidates; missing or incomplete comparisons produce unresolved findings, never causal claims. Returns every finding inline.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "comparisons": [\n {\n "authority": "analyst-inference",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_cc8049f629a9318e985bee4a547c0ec6646e9d3c2cbbabf4ac9b3758bc3ede86",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "changes": [\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "pseudocode",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 9,\n "left_digest": "64abdfe133a114158fa0e185d0edec784e6ccf13a166a0fed8bc18bb0cc72554",\n "limitations": [],\n "right_count": 9,\n "right_digest": "c8e26d77640903b2d4c765ebcb7f7170b64dd320dd3911abbe8346e07cd593d1",\n "status": "changed",\n "text_delta": {\n "added_lines": 1,\n "hunks": 2,\n "removed_lines": 1\n }\n }\n ],\n "dimensions": [\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "identity",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": null,\n "left_digest": "dd190a914de1bdf8f4bba97a2e74b439585874c567f28eb0ff41bc5388c66d65",\n "limitations": [],\n "right_count": null,\n "right_digest": "dd190a914de1bdf8f4bba97a2e74b439585874c567f28eb0ff41bc5388c66d65",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "pseudocode",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 9,\n "left_digest": "64abdfe133a114158fa0e185d0edec784e6ccf13a166a0fed8bc18bb0cc72554",\n "limitations": [],\n "right_count": 9,\n "right_digest": "c8e26d77640903b2d4c765ebcb7f7170b64dd320dd3911abbe8346e07cd593d1",\n "status": "changed",\n "text_delta": {\n "added_lines": 1,\n "hunks": 2,\n "removed_lines": 1\n }\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "assembly",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",\n "limitations": [\n "Assembly is opaque provider text; relocation normalization is unavailable."\n ],\n "right_count": 0,\n "right_digest": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",\n "status": "unchanged",\n "text_delta": {\n "added_lines": 0,\n "hunks": 0,\n "removed_lines": 0\n }\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "comments",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "calls",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "references",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "strings_names",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "cfg",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 1,\n "left_digest": "e947121f0d4cb8d2a2468a75a44872a29aa1239e6401712df80cfa36bf7df7a0",\n "limitations": [],\n "right_count": 1,\n "right_digest": "e947121f0d4cb8d2a2468a75a44872a29aa1239e6401712df80cfa36bf7df7a0",\n "status": "unchanged",\n "text_delta": null\n }\n ],\n "function_match": {\n "left_name": "main",\n "method": "symbol",\n "right_name": "main",\n "status": "matched"\n },\n "left_subject_sha256": "0000000000000000000000000000000000000000000000000000000000000000",\n "limitations": [],\n "right_subject_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "status": "changed",\n "summary": {\n "changed": 1,\n "truncated": 0,\n "unchanged": 7,\n "unknown": 0\n }\n },\n "operation": "compare_functions",\n "parameters": {\n "left_evidence_id": "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "right_evidence_id": "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n },\n "predicate_type": "rea.function-comparison",\n "provider": {\n "id": "rea-function-comparison",\n "name": "REA function comparison",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n ]\n }\n ],\n "properties": {\n "comparisons": {\n "default": [],\n "description": "Validated comparison Evidence records to aggregate.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "type": "object"\n },\n "kind": "session",\n "name": "find_changed_behavior",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "behavior_status": {\n "enum": [\n "observed_changed",\n "observed_unchanged",\n "unknown",\n "truncated"\n ],\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 3,\n "type": "array"\n },\n "findings": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "enum": [\n "observed_change",\n "derived_relationship",\n "contradiction",\n "unresolved_branch"\n ],\n "type": "string"\n },\n "dimension": {\n "minLength": 1,\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 3,\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "scope": {\n "enum": [\n "runtime",\n "protocol",\n "resource",\n "static_candidate"\n ],\n "type": "string"\n },\n "source_comparison_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n }\n },\n "required": [\n "scope",\n "dimension",\n "classification",\n "status",\n "source_comparison_id",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "items"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "contradictions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "observed_changes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "static_candidates": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unresolved": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "observed_changes",\n "static_candidates",\n "contradictions",\n "unresolved"\n ],\n "type": "object"\n }\n },\n "required": [\n "behavior_status",\n "summary",\n "findings",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Find Changed Behavior"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Build every shortest direct-callee path inline from complete analyze_function Evidence records using exact canonical addresses. Missing dossiers and provider mixing remain unknown; every node and edge cites source Evidence.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "functions": [\n {\n "authority": "shipped-artifact",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "evidence_links": [],\n "limitations": [],\n "locations": [],\n "normalized_result": {\n "assembly": [],\n "basic_blocks": [\n {\n "end": "0x1001",\n "start": "0x1000",\n "successors": []\n }\n ],\n "callees": [],\n "callers": [],\n "comments": [],\n "incoming_references": [],\n "outgoing_references": [],\n "procedure": {\n "address": "0x1000",\n "locals": [],\n "name": "main",\n "signature": "int main(void)"\n },\n "pseudocode": "return 0;",\n "referenced_names": [],\n "referenced_strings": []\n },\n "operation": "analyze_function",\n "parameters": {\n "procedure": "main"\n },\n "predicate_type": "rea.analysis",\n "provider": {\n "id": "rea-workflow",\n "name": "REA composed investigation workflow",\n "version": "1"\n },\n "raw_result": null,\n "subject": {\n "architecture": null,\n "digest": {\n "sha256": "0000000000000000000000000000000000000000000000000000000000000000"\n },\n "format": "mach-o",\n "local_path": "/tmp/function-0",\n "name": "function-0"\n }\n }\n ],\n "goal": {\n "address": "0x1000"\n },\n "start": {\n "address": "0x1000"\n }\n }\n ],\n "properties": {\n "functions": {\n "description": "Value for functions.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "goal": {\n "additionalProperties": false,\n "description": "Value for goal.",\n "properties": {\n "address": {\n "description": "Value for address.",\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9a-fA-F]{2})+:)?0[xX][0-9a-fA-F]+$",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "start": {\n "additionalProperties": false,\n "description": "Value for start.",\n "properties": {\n "address": {\n "description": "Value for address.",\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9a-fA-F]{2})+:)?0[xX][0-9a-fA-F]+$",\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n }\n },\n "required": [\n "functions",\n "start",\n "goal"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "build_call_path",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "explored": {\n "additionalProperties": false,\n "properties": {\n "depth_reached": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "nodes",\n "edges",\n "depth_reached"\n ],\n "type": "object"\n },\n "goal": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "paths": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "edges": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "source": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "target": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n }\n },\n "required": [\n "source",\n "target",\n "evidence_links"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "hops": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nodes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "address",\n "name",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "hops",\n "nodes",\n "edges",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "search_scope": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "type": "boolean"\n }\n },\n "required": [\n "exhaustive"\n ],\n "type": "object"\n },\n "shortest_hops": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "start": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "status": {\n "const": "found",\n "type": "string"\n }\n },\n "required": [\n "start",\n "goal",\n "explored",\n "evidence_links",\n "limitations",\n "status",\n "shortest_hops",\n "search_scope",\n "paths"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "explored": {\n "additionalProperties": false,\n "properties": {\n "depth_reached": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "nodes",\n "edges",\n "depth_reached"\n ],\n "type": "object"\n },\n "goal": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "paths": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "search_scope": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "const": true,\n "type": "boolean"\n }\n },\n "required": [\n "exhaustive"\n ],\n "type": "object"\n },\n "shortest_hops": {\n "type": "null"\n },\n "start": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "status": {\n "const": "not_found",\n "type": "string"\n }\n },\n "required": [\n "start",\n "goal",\n "explored",\n "evidence_links",\n "limitations",\n "status",\n "shortest_hops",\n "search_scope",\n "paths"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "explored": {\n "additionalProperties": false,\n "properties": {\n "depth_reached": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "edges": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "nodes": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "nodes",\n "edges",\n "depth_reached"\n ],\n "type": "object"\n },\n "goal": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "paths": {\n "items": {\n "not": {}\n },\n "maxItems": 0,\n "minItems": 0,\n "type": "array"\n },\n "search_scope": {\n "additionalProperties": false,\n "properties": {\n "exhaustive": {\n "const": false,\n "type": "boolean"\n }\n },\n "required": [\n "exhaustive"\n ],\n "type": "object"\n },\n "shortest_hops": {\n "type": "null"\n },\n "start": {\n "pattern": "^(?:(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$",\n "type": "string"\n },\n "status": {\n "const": "unknown",\n "type": "string"\n }\n },\n "required": [\n "start",\n "goal",\n "explored",\n "evidence_links",\n "limitations",\n "status",\n "shortest_hops",\n "search_scope",\n "paths"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Build Call Path"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Evaluate every explicit caller-declared hypothesis between exact static comparison findings and runtime comparison dimensions. Similar names or paths are never auto-matched, consistent cochange never proves causality, and unknown or truncated inputs remain unresolved. Returns all correlations and their complete Evidence closure inline.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "mappings": [\n {\n "hypothesis": {\n "expected_pattern": "static_only",\n "statement": "Static implementation changed without terminal change."\n },\n "runtime": {\n "comparison_evidence_id": "ev_7b90781a4b4c9006d4eee122009b3baed342e442ad2ad62498bf9e7df850355c",\n "dimension": "terminal"\n },\n "side_alignment": "left_to_left",\n "static": {\n "comparison_evidence_id": "ev_cc8049f629a9318e985bee4a547c0ec6646e9d3c2cbbabf4ac9b3758bc3ede86",\n "selector": {\n "dimension": "pseudocode",\n "kind": "function"\n }\n }\n }\n ],\n "runtime_comparisons": [\n {\n "authority": "analyst-inference",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_7b90781a4b4c9006d4eee122009b3baed342e442ad2ad62498bf9e7df850355c",\n "evidence_links": [\n "ev_39dc6ca57fd8d981c0bce63a1de75dd752e0ae47ff911440949cba8f6a8f1b7c",\n "ev_434bd49ec7096a6a955adb869d0f38ae3cb62f1256376ca87ca7ca983e5ef538"\n ],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "exit": "unchanged",\n "filesystem": "unchanged",\n "first_divergence": {\n "status": "none"\n },\n "interaction": "unchanged",\n "limitations": [],\n "process": "unchanged",\n "status": "unchanged",\n "terminal": "unchanged"\n },\n "operation": "compare_process_captures",\n "parameters": {\n "left_evidence_id": "ev_39dc6ca57fd8d981c0bce63a1de75dd752e0ae47ff911440949cba8f6a8f1b7c",\n "left_normalization": {\n "paths": true,\n "patterns": [],\n "pids": true,\n "ports": true,\n "time_bucket_ms": 10\n },\n "right_evidence_id": "ev_434bd49ec7096a6a955adb869d0f38ae3cb62f1256376ca87ca7ca983e5ef538",\n "right_normalization": {\n "paths": true,\n "patterns": [],\n "pids": true,\n "ports": true,\n "time_bucket_ms": 10\n }\n },\n "predicate_type": "rea.process-comparison",\n "provider": {\n "id": "rea-process",\n "name": "REA deterministic process harness",\n "version": "3"\n },\n "raw_result": null,\n "subject": null\n }\n ],\n "static_comparisons": [\n {\n "authority": "analyst-inference",\n "confidence": "derived",\n "environment": null,\n "evidence_id": "ev_cc8049f629a9318e985bee4a547c0ec6646e9d3c2cbbabf4ac9b3758bc3ede86",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "limitations": [\n "Artifact identity is unavailable for this observation."\n ],\n "locations": [],\n "normalized_result": {\n "changes": [\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "pseudocode",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 9,\n "left_digest": "64abdfe133a114158fa0e185d0edec784e6ccf13a166a0fed8bc18bb0cc72554",\n "limitations": [],\n "right_count": 9,\n "right_digest": "c8e26d77640903b2d4c765ebcb7f7170b64dd320dd3911abbe8346e07cd593d1",\n "status": "changed",\n "text_delta": {\n "added_lines": 1,\n "hunks": 2,\n "removed_lines": 1\n }\n }\n ],\n "dimensions": [\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "identity",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": null,\n "left_digest": "dd190a914de1bdf8f4bba97a2e74b439585874c567f28eb0ff41bc5388c66d65",\n "limitations": [],\n "right_count": null,\n "right_digest": "dd190a914de1bdf8f4bba97a2e74b439585874c567f28eb0ff41bc5388c66d65",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "pseudocode",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 9,\n "left_digest": "64abdfe133a114158fa0e185d0edec784e6ccf13a166a0fed8bc18bb0cc72554",\n "limitations": [],\n "right_count": 9,\n "right_digest": "c8e26d77640903b2d4c765ebcb7f7170b64dd320dd3911abbe8346e07cd593d1",\n "status": "changed",\n "text_delta": {\n "added_lines": 1,\n "hunks": 2,\n "removed_lines": 1\n }\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "assembly",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",\n "limitations": [\n "Assembly is opaque provider text; relocation normalization is unavailable."\n ],\n "right_count": 0,\n "right_digest": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",\n "status": "unchanged",\n "text_delta": {\n "added_lines": 0,\n "hunks": 0,\n "removed_lines": 0\n }\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "comments",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "calls",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "references",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "strings_names",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 0,\n "left_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "limitations": [],\n "right_count": 0,\n "right_digest": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",\n "status": "unchanged",\n "text_delta": null\n },\n {\n "conclusion_kind": "derived_relationship",\n "dimension": "cfg",\n "evidence_links": [\n "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n ],\n "left_count": 1,\n "left_digest": "e947121f0d4cb8d2a2468a75a44872a29aa1239e6401712df80cfa36bf7df7a0",\n "limitations": [],\n "right_count": 1,\n "right_digest": "e947121f0d4cb8d2a2468a75a44872a29aa1239e6401712df80cfa36bf7df7a0",\n "status": "unchanged",\n "text_delta": null\n }\n ],\n "function_match": {\n "left_name": "main",\n "method": "symbol",\n "right_name": "main",\n "status": "matched"\n },\n "left_subject_sha256": "0000000000000000000000000000000000000000000000000000000000000000",\n "limitations": [],\n "right_subject_sha256": "1111111111111111111111111111111111111111111111111111111111111111",\n "status": "changed",\n "summary": {\n "changed": 1,\n "truncated": 0,\n "unchanged": 7,\n "unknown": 0\n }\n },\n "operation": "compare_functions",\n "parameters": {\n "left_evidence_id": "ev_fd7f0f3851d21e5284b47129d9c7142e2ebeb6ed1c5f91bcff18a781d0b24cb7",\n "right_evidence_id": "ev_79f87eddd8df1d59c6f2e2cd99f21bfae2e5a22fd26c5bf0d0b9ccf9e24b0522"\n },\n "predicate_type": "rea.function-comparison",\n "provider": {\n "id": "rea-function-comparison",\n "name": "REA function comparison",\n "version": "1"\n },\n "raw_result": null,\n "subject": null\n }\n ]\n }\n ],\n "properties": {\n "mappings": {\n "description": "Value for mappings.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "hypothesis": {\n "additionalProperties": false,\n "description": "Value for hypothesis.",\n "properties": {\n "expected_pattern": {\n "description": "Expected pattern used to reject stale or mismatched input.",\n "enum": [\n "cochanged",\n "static_only",\n "runtime_only",\n "both_unchanged"\n ],\n "type": "string"\n },\n "statement": {\n "description": "Value for statement.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "statement",\n "expected_pattern"\n ],\n "type": "object"\n },\n "runtime": {\n "additionalProperties": false,\n "description": "Value for runtime.",\n "properties": {\n "comparison_evidence_id": {\n "description": "Exact Evidence identifier for the comparison observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "dimension": {\n "description": "Value for dimension.",\n "enum": [\n "terminal",\n "interaction",\n "exit",\n "filesystem",\n "process"\n ],\n "type": "string"\n }\n },\n "required": [\n "comparison_evidence_id",\n "dimension"\n ],\n "type": "object"\n },\n "side_alignment": {\n "description": "Value for side alignment.",\n "enum": [\n "left_to_left",\n "left_to_right"\n ],\n "type": "string"\n },\n "static": {\n "additionalProperties": false,\n "description": "Value for static.",\n "properties": {\n "comparison_evidence_id": {\n "description": "Exact Evidence identifier for the comparison observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "selector": {\n "description": "Value for selector.",\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact",\n "description": "Value for kind.",\n "type": "string"\n },\n "logical_path": {\n "description": "Local filesystem path for logical.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "logical_path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimension": {\n "description": "Value for dimension.",\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "kind": {\n "const": "function",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "dimension"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "comparison_evidence_id",\n "selector"\n ],\n "type": "object"\n }\n },\n "required": [\n "static",\n "runtime",\n "side_alignment",\n "hypothesis"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "runtime_comparisons": {\n "description": "Value for runtime comparisons.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n },\n "static_comparisons": {\n "description": "Value for static comparisons.",\n "items": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "description": "Value for analysis profile.",\n "properties": {\n "digest": {\n "description": "Value for digest.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "description": "Value for version.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "description": "Value for authority.",\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "description": "Value for confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "description": "Value for architecture.",\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "description": "Value for isolation.",\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "description": "Value for platform.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for environment."\n },\n "evidence_id": {\n "description": "Stable identifier of the recorded Evidence observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "description": "Value for evidence links.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "description": "Value for limitations.",\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "description": "Value for locations.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "description": "Value for kind.",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "artifact-path",\n "description": "Value for kind.",\n "type": "string"\n },\n "path": {\n "description": "Local filesystem path used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "properties": {\n "kind": {\n "const": "file-offset",\n "description": "Value for kind.",\n "type": "string"\n },\n "offset": {\n "description": "Value for offset.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "properties": {\n "end": {\n "description": "Value for end.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "description": "Value for kind.",\n "type": "string"\n },\n "start": {\n "description": "Value for start.",\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0",\n "description": "Value for normalized result."\n },\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "description": "Value for parameters.",\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "description": "Value for predicate type.",\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "description": "Value for provider.",\n "properties": {\n "id": {\n "description": "Value for id.",\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for version."\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for raw result."\n },\n "subject": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "digest": {\n "description": "Value for digest.",\n "properties": {\n "sha256": {\n "description": "Value for sha256.",\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "description": "Declared input artifact format.",\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "description": "Local filesystem path for local.",\n "type": "string"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for subject."\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "minItems": 1,\n "type": "array"\n }\n },\n "required": [\n "static_comparisons",\n "runtime_comparisons",\n "mappings"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "correlate_static_and_runtime",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "correlations": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "classification": {\n "enum": [\n "hypothesis",\n "contradiction",\n "unresolved_branch"\n ],\n "type": "string"\n },\n "correlation_id": {\n "pattern": "^cor_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "hypothesis": {\n "additionalProperties": false,\n "properties": {\n "expected_pattern": {\n "enum": [\n "cochanged",\n "static_only",\n "runtime_only",\n "both_unchanged"\n ],\n "type": "string"\n },\n "statement": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "statement",\n "expected_pattern"\n ],\n "type": "object"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "observed_pattern": {\n "enum": [\n "cochanged",\n "static_only",\n "runtime_only",\n "both_unchanged",\n "unknown",\n "truncated"\n ],\n "type": "string"\n },\n "runtime": {\n "additionalProperties": false,\n "properties": {\n "comparison_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "dimension": {\n "enum": [\n "terminal",\n "interaction",\n "exit",\n "filesystem",\n "process"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n }\n },\n "required": [\n "comparison_evidence_id",\n "dimension",\n "status"\n ],\n "type": "object"\n },\n "side_alignment": {\n "enum": [\n "left_to_left",\n "left_to_right"\n ],\n "type": "string"\n },\n "static": {\n "additionalProperties": false,\n "properties": {\n "comparison_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "conclusion_kind": {\n "enum": [\n "derived_relationship",\n "contradiction",\n "unresolved_branch"\n ],\n "type": "string"\n },\n "selector": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact",\n "type": "string"\n },\n "logical_path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "logical_path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "dimension": {\n "enum": [\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "kind": {\n "const": "function",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "dimension"\n ],\n "type": "object"\n }\n ]\n },\n "status": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n }\n },\n "required": [\n "comparison_evidence_id",\n "selector",\n "status",\n "conclusion_kind"\n ],\n "type": "object"\n }\n },\n "required": [\n "correlation_id",\n "static",\n "runtime",\n "side_alignment",\n "hypothesis",\n "observed_pattern",\n "classification",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "items"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 2,\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "status": {\n "enum": [\n "correlated",\n "contradicted",\n "unknown",\n "truncated"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "contradictions": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "hypotheses": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "unresolved": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "hypotheses",\n "contradictions",\n "unresolved"\n ],\n "type": "object"\n }\n },\n "required": [\n "status",\n "summary",\n "correlations",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Correlate Static And Runtime"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Verify a finite typed behavioral and structural specification against a canonical Evidence bundle. Pass means every declared claim has complete comparable authority—not global source equivalence; changed claims fail and missing, limited, or unresolved evidence stays unknown.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "specification": {\n "claims": [\n {\n "claim_id": "terminal-output",\n "comparison_evidence_id": "ev_7b90781a4b4c9006d4eee122009b3baed342e442ad2ad62498bf9e7df850355c",\n "dimension": "terminal",\n "kind": "behavioral",\n "title": "Terminal output remains equivalent"\n }\n ],\n "name": "Terminal compatibility"\n }\n }\n ],\n "properties": {\n "specification": {\n "description": "Value for specification.",\n "properties": {\n "claims": {\n "description": "Value for claims.",\n "items": {\n "oneOf": [\n {\n "properties": {\n "claim_id": {\n "description": "Value for claim id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "comparison_evidence_id": {\n "description": "Exact Evidence identifier for the comparison observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "dimension": {\n "description": "Value for dimension.",\n "enum": [\n "overall",\n "terminal",\n "interaction",\n "exit",\n "filesystem",\n "process"\n ],\n "type": "string"\n },\n "kind": {\n "const": "behavioral",\n "description": "Value for kind.",\n "type": "string"\n },\n "title": {\n "description": "Value for title.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "claim_id",\n "title",\n "comparison_evidence_id",\n "kind",\n "dimension"\n ],\n "type": "object"\n },\n {\n "properties": {\n "claim_id": {\n "description": "Value for claim id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "comparison_evidence_id": {\n "description": "Exact Evidence identifier for the comparison observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "dimension": {\n "description": "Value for dimension.",\n "enum": [\n "overall",\n "identity",\n "pseudocode",\n "assembly",\n "comments",\n "calls",\n "references",\n "strings_names",\n "cfg"\n ],\n "type": "string"\n },\n "kind": {\n "const": "structural-function",\n "description": "Value for kind.",\n "type": "string"\n },\n "title": {\n "description": "Value for title.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "claim_id",\n "title",\n "comparison_evidence_id",\n "kind",\n "dimension"\n ],\n "type": "object"\n },\n {\n "properties": {\n "claim_id": {\n "description": "Value for claim id.",\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "comparison_evidence_id": {\n "description": "Exact Evidence identifier for the comparison observation.",\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "dimension": {\n "const": "overall",\n "description": "Value for dimension.",\n "type": "string"\n },\n "kind": {\n "const": "structural-artifact",\n "description": "Value for kind.",\n "type": "string"\n },\n "title": {\n "description": "Value for title.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "claim_id",\n "title",\n "comparison_evidence_id",\n "kind",\n "dimension"\n ],\n "type": "object"\n }\n ]\n },\n "minItems": 1,\n "type": "array"\n },\n "name": {\n "description": "Exact name used by this operation.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "claims"\n ],\n "type": "object"\n }\n },\n "required": [\n "specification"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "verify_reconstruction",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "evidence": {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "result": {\n "additionalProperties": false,\n "properties": {\n "claims": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "claim_id": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "comparison_evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "dimension": {\n "minLength": 1,\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 3,\n "type": "array"\n },\n "kind": {\n "enum": [\n "behavioral",\n "structural-function",\n "structural-artifact"\n ],\n "type": "string"\n },\n "left_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "observed_status": {\n "enum": [\n "unchanged",\n "added",\n "removed",\n "changed",\n "truncated",\n "unknown",\n "contradiction"\n ],\n "type": "string"\n },\n "right_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "status": {\n "enum": [\n "pass",\n "fail",\n "unknown"\n ],\n "type": "string"\n },\n "unknown_ids": {\n "items": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "claim_id",\n "kind",\n "dimension",\n "status",\n "observed_status",\n "comparison_evidence_id",\n "left_evidence_ids",\n "right_evidence_ids",\n "evidence_links",\n "unknown_ids",\n "limitations"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "items"\n ],\n "type": "object"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 3,\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "claim_ids": {\n "items": {\n "pattern": "^[A-Za-z][A-Za-z0-9._-]*$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n },\n "unknown_ids": {\n "items": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "operation",\n "rationale",\n "claim_ids",\n "unknown_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "specification_sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "status": {\n "enum": [\n "pass",\n "fail",\n "unknown"\n ],\n "type": "string"\n },\n "summary": {\n "additionalProperties": false,\n "properties": {\n "behavioral": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "failed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "passed": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "structural": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "unknown": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "total",\n "passed",\n "failed",\n "unknown",\n "behavioral",\n "structural"\n ],\n "type": "object"\n }\n },\n "required": [\n "status",\n "specification_sha256",\n "summary",\n "claims",\n "recommended_probes",\n "evidence_links",\n "limitations"\n ],\n "type": "object"\n }\n },\n "required": [\n "result",\n "evidence_id",\n "evidence"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Verify Reconstruction"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": true\n },\n "description": "List every current residual-unknown head in deterministic ID order, with optional exact status, severity, and domain filters. Results are complete and inline. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": false,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {\n "domain": {\n "description": "Value for domain.",\n "minLength": 1,\n "type": "string"\n },\n "severity": {\n "description": "Value for severity.",\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "session",\n "name": "list_unknowns",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "items": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "contradicting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "question": {\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "type": {\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "required_confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "const": "verified",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ]\n },\n "revision": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "severity": {\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "total": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "items",\n "total"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "List Unknowns"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Create one deterministic residual unknown and immutable mutation evidence. Validates all evidence and relationship references, and rejects duplicate stable identity.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "contradicting_evidence_ids": [],\n "domain": "protocol",\n "question": "Does this branch require an unavailable external service?",\n "recommended_probes": [\n {\n "operation": "capture_process_scenario",\n "rationale": "Replay branch."\n }\n ],\n "relationships": [],\n "required_authority": "controlled-replay",\n "required_confidence": "observed",\n "required_environment": null,\n "severity": "medium",\n "supporting_evidence_ids": []\n }\n ],\n "properties": {\n "contradicting_evidence_ids": {\n "default": [],\n "description": "Ordered Evidence identifiers for the contradicting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "description": "Value for domain.",\n "minLength": 1,\n "type": "string"\n },\n "question": {\n "description": "Concrete unresolved question to retain for later investigation.",\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "description": "Value for recommended probes.",\n "items": {\n "properties": {\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "description": "Value for relationships.",\n "items": {\n "properties": {\n "type": {\n "description": "Value for type.",\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required authority."\n },\n "required_confidence": {\n "description": "Value for required confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for id."\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for isolation."\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for platform."\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required environment."\n },\n "severity": {\n "description": "Value for severity.",\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "default": [],\n "description": "Ordered Evidence identifiers for the supporting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n }\n },\n "required": [\n "question",\n "severity",\n "domain",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "record_unknown",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "contradicting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "question": {\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "type": {\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "required_confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "const": "verified",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ]\n },\n "revision": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "severity": {\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Record Unknown"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": false,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Append one immutable full-state revision and mutation evidence. Requires exact expected_revision; stale concurrent writers fail instead of overwriting newer analysis.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": false,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "examples": [\n {\n "contradicting_evidence_ids": [],\n "expected_revision": 1,\n "recommended_probes": [],\n "relationships": [],\n "required_authority": "controlled-replay",\n "required_confidence": "observed",\n "required_environment": null,\n "resolution": null,\n "severity": "medium",\n "status": "investigating",\n "supporting_evidence_ids": [],\n "unknown_id": "unk_0000000000000000000000000000000000000000000000000000000000000000"\n }\n ],\n "properties": {\n "contradicting_evidence_ids": {\n "description": "Ordered Evidence identifiers for the contradicting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "expected_revision": {\n "description": "Expected revision used to reject stale or mismatched input.",\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "recommended_probes": {\n "description": "Value for recommended probes.",\n "items": {\n "properties": {\n "operation": {\n "description": "Value for operation.",\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "description": "Value for relationships.",\n "items": {\n "properties": {\n "type": {\n "description": "Value for type.",\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required authority."\n },\n "required_confidence": {\n "description": "Value for required confidence.",\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for architecture."\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for id."\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for isolation."\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for platform."\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for required environment."\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "properties": {\n "disposition": {\n "const": "verified",\n "description": "Value for disposition.",\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "properties": {\n "disposition": {\n "description": "Value for disposition.",\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "description": "Value for evidence ids.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "description": "Value for rationale.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ],\n "description": "Value for resolution."\n },\n "severity": {\n "description": "Value for severity.",\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "description": "Current lifecycle or verification status.",\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "description": "Ordered Evidence identifiers for the supporting observations.",\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "expected_revision",\n "status",\n "severity",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "update_unknown",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "contradicting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "question": {\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "type": {\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "required_confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "const": "verified",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ]\n },\n "revision": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "severity": {\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Update Unknown"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": true\n },\n "description": "Revalidate the current residual-unknown head against live bundled evidence, exact authority/confidence/environment requirements, and revision integrity. Withdrawn and out-of-scope dispositions are not truth claims.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": false,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "unknown_id": "unk_0000000000000000000000000000000000000000000000000000000000000000"\n }\n ],\n "properties": {\n "unknown_id": {\n "description": "Exact residual-unknown identifier.",\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "verify_unknown_resolution",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "truthVerified": {\n "type": "boolean"\n },\n "unknown": {\n "additionalProperties": false,\n "properties": {\n "contradicting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "question": {\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "type": {\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "required_confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "const": "verified",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ]\n },\n "revision": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "severity": {\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n },\n "valid": {\n "type": "boolean"\n }\n },\n "required": [\n "valid",\n "truthVerified",\n "unknown"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Verify Unknown Resolution"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": true\n },\n "description": "Return every Evidence record and residual unknown currently retained by this session as one inline bundle for direct inspection or follow-up workflows.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": false,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {},\n "type": "object"\n },\n "kind": "session",\n "name": "get_evidence_bundle",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "artifacts": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n }\n },\n "required": [\n "digest",\n "format",\n "architecture"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "captures": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "predicate_type"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "environments": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "providers": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "records": {\n "items": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "analysis_profile": {\n "additionalProperties": false,\n "properties": {\n "digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n }\n },\n "required": [\n "provider",\n "parameters",\n "digest"\n ],\n "type": "object"\n },\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links",\n "analysis_profile"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n "confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "minLength": 1,\n "type": "string"\n },\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "isolation": {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n "platform": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "evidence_links": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "limitations": {\n "items": {\n "type": "string"\n },\n "type": "array"\n },\n "locations": {\n "items": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "address": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address",\n "type": "string"\n }\n },\n "required": [\n "kind",\n "address"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "minLength": 1,\n "type": "string"\n },\n "kind": {\n "const": "address-range",\n "type": "string"\n },\n "start": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "artifact-path",\n "type": "string"\n },\n "path": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "kind",\n "path"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "kind": {\n "const": "file-offset",\n "type": "string"\n },\n "offset": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "offset"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "end": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n },\n "kind": {\n "const": "file-offset-range",\n "type": "string"\n },\n "start": {\n "maximum": 9007199254740991,\n "minimum": 0,\n "type": "integer"\n }\n },\n "required": [\n "kind",\n "start",\n "end"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "normalized_result": {\n "$ref": "#/$defs/__schema0"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "parameters": {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n },\n "predicate_type": {\n "minLength": 1,\n "type": "string"\n },\n "provider": {\n "additionalProperties": false,\n "properties": {\n "id": {\n "minLength": 1,\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n },\n "version": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "name",\n "version"\n ],\n "type": "object"\n },\n "raw_result": {\n "anyOf": [\n {\n "$ref": "#/$defs/__schema0"\n },\n {\n "type": "null"\n }\n ]\n },\n "subject": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "enum": [\n "x86",\n "x86_64",\n "arm",\n "arm64"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "digest": {\n "additionalProperties": false,\n "properties": {\n "sha256": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "sha256"\n ],\n "type": "object"\n },\n "format": {\n "enum": [\n "hopper",\n "analysis-database",\n "mach-o",\n "elf",\n "pe",\n "dos-mz",\n "dos-com",\n "zip",\n "ipa",\n "apk",\n "msix",\n "appx",\n "asar",\n "dmg",\n "pkg",\n "plist",\n "javascript",\n "source-map",\n "directory",\n "file",\n "unknown",\n "mach-o-universal",\n "javascript-bundle",\n "entitlements"\n ],\n "type": "string"\n },\n "local_path": {\n "type": "string"\n },\n "name": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "name",\n "digest",\n "format",\n "architecture",\n "local_path"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "evidence_id",\n "subject",\n "provider",\n "predicate_type",\n "operation",\n "parameters",\n "raw_result",\n "normalized_result",\n "confidence",\n "authority",\n "environment",\n "limitations",\n "locations",\n "evidence_links"\n ],\n "type": "object"\n }\n ]\n },\n "type": "array"\n },\n "scenarios": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "authority": {\n "const": "controlled-replay",\n "type": "string"\n },\n "evidence_id": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "operation": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "evidence_id",\n "operation",\n "authority"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "unknowns": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "contradicting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "domain": {\n "minLength": 1,\n "type": "string"\n },\n "mutation_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "previous_revision_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "question": {\n "minLength": 1,\n "type": "string"\n },\n "recommended_probes": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "operation": {\n "minLength": 1,\n "type": "string"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "operation",\n "rationale"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "relationships": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "type": {\n "enum": [\n "depends-on",\n "related-to",\n "contradicts"\n ],\n "type": "string"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "type",\n "unknown_id"\n ],\n "type": "object"\n },\n "type": "array"\n },\n "required_authority": {\n "anyOf": [\n {\n "enum": [\n "shipped-artifact",\n "controlled-replay",\n "historical-reference",\n "external-service",\n "analyst-inference"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "required_confidence": {\n "enum": [\n "observed",\n "derived",\n "inferred"\n ],\n "type": "string"\n },\n "required_environment": {\n "anyOf": [\n {\n "additionalProperties": false,\n "properties": {\n "architecture": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "id": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "isolation": {\n "anyOf": [\n {\n "enum": [\n "none",\n "process",\n "container",\n "virtual-machine"\n ],\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "platform": {\n "anyOf": [\n {\n "minLength": 1,\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "id",\n "platform",\n "architecture",\n "isolation"\n ],\n "type": "object"\n },\n {\n "type": "null"\n }\n ]\n },\n "resolution": {\n "anyOf": [\n {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "const": "verified",\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "minItems": 1,\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "disposition": {\n "enum": [\n "withdrawn",\n "out-of-scope"\n ],\n "type": "string"\n },\n "evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "rationale": {\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "disposition",\n "rationale",\n "evidence_ids"\n ],\n "type": "object"\n }\n ]\n },\n {\n "type": "null"\n }\n ]\n },\n "revision": {\n "maximum": 9007199254740991,\n "minimum": 1,\n "type": "integer"\n },\n "revision_digest": {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n "scope_digest": {\n "anyOf": [\n {\n "pattern": "^[a-f0-9]{64}$",\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "severity": {\n "enum": [\n "low",\n "medium",\n "high",\n "critical"\n ],\n "type": "string"\n },\n "status": {\n "enum": [\n "open",\n "investigating",\n "blocked",\n "contradicted",\n "resolved"\n ],\n "type": "string"\n },\n "supporting_evidence_ids": {\n "items": {\n "pattern": "^ev_[a-f0-9]{64}$",\n "type": "string"\n },\n "type": "array"\n },\n "unknown_id": {\n "pattern": "^unk_[a-f0-9]{64}$",\n "type": "string"\n }\n },\n "required": [\n "unknown_id",\n "revision",\n "previous_revision_digest",\n "revision_digest",\n "scope_digest",\n "question",\n "status",\n "severity",\n "domain",\n "supporting_evidence_ids",\n "contradicting_evidence_ids",\n "required_authority",\n "required_confidence",\n "required_environment",\n "recommended_probes",\n "relationships",\n "resolution",\n "mutation_evidence_ids"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "artifacts",\n "providers",\n "environments",\n "scenarios",\n "captures",\n "unknowns",\n "records"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Get Evidence Bundle"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Return the selected document, current address, and containing/current procedure in one provider-neutral result. The result reflects sequential provider observations, not an atomic cursor snapshot; a cursor outside any procedure returns procedure: null.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {}\n ],\n "properties": {\n "document": {\n "description": "Exact provider document or program identity.",\n "minLength": 1,\n "type": "string"\n }\n },\n "type": "object"\n },\n "kind": "session",\n "name": "get_navigation_context",\n "outputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "document": {\n "type": "string"\n },\n "procedure": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n }\n },\n "required": [\n "document",\n "address",\n "procedure"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Get Navigation Context"\n },\n {\n "analysisOperation": null,\n "annotations": {\n "destructiveHint": false,\n "idempotentHint": true,\n "openWorldHint": false,\n "readOnlyHint": false\n },\n "description": "Inspect one explicit reproducible address for its analyzed name, containing procedure, regular and inline comments, and matching bookmarks. Each unsupported facet returns a typed unavailable outcome; use xrefs, assembly, or pseudocode for those deeper views.",\n "effects": {\n "accessesNetwork": false,\n "changesUiState": false,\n "idempotent": true,\n "launchesProcess": false,\n "mayDiscardData": false,\n "mutatesSession": true,\n "mutatesTarget": false,\n "writesFilesystem": false\n },\n "inputSchema": {\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "examples": [\n {\n "address": "0x1000"\n }\n ],\n "properties": {\n "address": {\n "description": "Value for address.",\n "minLength": 1,\n "type": "string"\n },\n "document": {\n "description": "Exact provider document or program identity.",\n "minLength": 1,\n "type": "string"\n }\n },\n "required": [\n "address"\n ],\n "type": "object"\n },\n "kind": "session",\n "name": "inspect_address_context",\n "outputSchema": {\n "$defs": {\n "__schema0": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "number"\n },\n {\n "type": "boolean"\n },\n {\n "type": "null"\n },\n {\n "items": {\n "$ref": "#/$defs/__schema0"\n },\n "type": "array"\n },\n {\n "additionalProperties": {\n "$ref": "#/$defs/__schema0"\n },\n "propertyNames": {\n "type": "string"\n },\n "type": "object"\n }\n ]\n }\n },\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "additionalProperties": false,\n "properties": {\n "result": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "bookmarks": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "available",\n "type": "string"\n },\n "value": {\n "items": {\n "additionalProperties": false,\n "properties": {\n "address": {\n "type": "string"\n },\n "name": {\n "type": "string"\n }\n },\n "required": [\n "address",\n "name"\n ],\n "type": "object"\n },\n "type": "array"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "remediation": {\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "comment": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "available",\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "remediation": {\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "document": {\n "anyOf": [\n {\n "type": "string"\n },\n {\n "type": "null"\n }\n ]\n },\n "inline_comment": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "available",\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "remediation": {\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "name": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "available",\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "remediation": {\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n },\n "procedure": {\n "oneOf": [\n {\n "additionalProperties": false,\n "properties": {\n "state": {\n "const": "available",\n "type": "string"\n },\n "value": {\n "$ref": "#/$defs/__schema0"\n }\n },\n "required": [\n "state",\n "value"\n ],\n "type": "object"\n },\n {\n "additionalProperties": false,\n "properties": {\n "reason": {\n "type": "string"\n },\n "remediation": {\n "type": "string"\n },\n "state": {\n "const": "unavailable",\n "type": "string"\n }\n },\n "required": [\n "state",\n "reason",\n "remediation"\n ],\n "type": "object"\n }\n ]\n }\n },\n "required": [\n "address",\n "document",\n "name",\n "procedure",\n "comment",\n "inline_comment",\n "bookmarks"\n ],\n "type": "object"\n }\n },\n "required": [\n "result"\n ],\n "type": "object"\n },\n "requiresSession": true,\n "title": "Inspect Address Context"\n }\n ]\n}'; -const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON); -if (!isGeneratedPayload(generatedPayload)) - throw new TypeError("Generated MCP catalog payload is invalid"); - -/** Generated from TOOL_CONTRACTS; do not edit. */ -export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog; - -/** Generated lightweight metadata for lazily loaded auxiliary providers. */ -export const GENERATED_AUXILIARY_PROVIDERS = - generatedPayload.auxiliaryProviders; - -function isGeneratedPayload(value: unknown): value is GeneratedPayload { - return ( - isRecord(value) && - Array.isArray(value.catalog) && - value.catalog.every(isGeneratedTool) && - Array.isArray(value.auxiliaryProviders) && - value.auxiliaryProviders.every(isGeneratedProvider) - ); -} - -function isGeneratedTool(value: unknown): value is GeneratedTool { - return ( - isRecord(value) && - typeof value.name === "string" && - (typeof value.analysisOperation === "string" || - value.analysisOperation === null) && - typeof value.title === "string" && - typeof value.description === "string" && - typeof value.kind === "string" && - typeof value.requiresSession === "boolean" && - isRecord(value.inputSchema) && - isRecord(value.outputSchema) && - isRecord(value.annotations) && - isRecord(value.effects) - ); -} - -function isGeneratedProvider(value: unknown): value is GeneratedProvider { - return ( - isRecord(value) && - isRecord(value.identity) && - Array.isArray(value.capabilities) - ); -} - -function isRecord(value: unknown): value is Readonly> { - return typeof value === "object" && value !== null && !Array.isArray(value); -} diff --git a/src/generatedPackageMetadata.ts b/src/generatedPackageMetadata.ts index cee394dcdb..538a2f0ab2 100644 --- a/src/generatedPackageMetadata.ts +++ b/src/generatedPackageMetadata.ts @@ -1,9 +1,9 @@ /** Generated from package.json and package-lock.json; do not edit. */ export const PACKAGE_METADATA = { name: "rea-agents", - version: "4.1.0", // x-release-please-version + version: "6.0.0", // x-release-please-version serverSdkVersion: "2.3.1", clientSdkVersion: "2.3.1", coreSdkVersion: "2.3.1", - skillVersion: "27", + skillVersion: "33", } as const; diff --git a/src/ghidra/GhidraFunctionValues.test.ts b/src/ghidra/GhidraFunctionValues.test.ts index 7914111d7a..f50a3759a5 100644 --- a/src/ghidra/GhidraFunctionValues.test.ts +++ b/src/ghidra/GhidraFunctionValues.test.ts @@ -299,23 +299,6 @@ describe("Ghidra jump-table mapping contract", () => { ).toMatchObject({ ok: false, error: { _tag: "AnalysisOutputError" } }); } }); - - it("keeps jump-table sources separate from case-to-target mappings", () => { - const parsed = parseGhidraFunctionResult( - "analyze_function", - ghidraFunctionDossier(), - ); - if (!parsed.ok) throw parsed.error; - const boundary = functionDossierSchema.parse(parsed.value).native_api; - if (boundary?.available !== true) - throw new TypeError("Ghidra native API fixture is unavailable"); - const mapping = boundary.jump_tables[0]?.mappings[0]; - expect(mapping).toMatchObject({ - case_value: 0, - target_address: "0x401020", - }); - expect(mapping).not.toHaveProperty("data_addresses"); - }); }); describe("Ghidra function-analysis malformed results", () => { diff --git a/src/ghidra/GhidraLauncher.ts b/src/ghidra/GhidraLauncher.ts index 82af60f411..444c4b523e 100644 --- a/src/ghidra/GhidraLauncher.ts +++ b/src/ghidra/GhidraLauncher.ts @@ -147,6 +147,7 @@ export class GhidraHeadlessLauncher implements GhidraLauncher { platform, command.command, ), + ...(options.signal === undefined ? {} : { signal: options.signal }), }); await writeGhidraRuntimeFile( paths.ownershipPath, diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 7bfddef409..9cc337c9ec 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -1,15 +1,19 @@ import { createHash } from "node:crypto"; +import { execFile } from "node:child_process"; // Fake-backed provider coverage; real Ghidra verification lives in // `npm run verify:ghidra` and its focused variants. -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import { fixtureDosLoadImage } from "./GhidraLoadImage.fixture.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { parseConfig } from "../config.js"; import type { BinaryTarget } from "../domain/binaryTarget.js"; +import { parseExecutableHeader } from "../domain/binaryTarget.js"; import { createAnalysisProfile } from "../domain/analysisProfile.js"; import { GHIDRA_PROVIDER_IDENTITY, @@ -257,14 +261,18 @@ describe("Ghidra platform support", () => { const ghidra = provider({ ...installationHost(), platform: "win32" }); const nativeApplication = peTarget("x86_64"); - expect(ghidra.inspectTargetSupport(nativeApplication)).toMatchObject({ - status: "supported", - diagnostics: { - host_platform: "win32", - executable_role: "application", - managed: false, - }, - }); + for (const architecture of ["x86", "x86_64"] as const) + expect(ghidra.inspectTargetSupport(peTarget(architecture))).toMatchObject( + { + status: "supported", + diagnostics: { + host_platform: "win32", + architecture, + executable_role: "application", + managed: false, + }, + }, + ); expect(ghidra.inspectAvailability()).toMatchObject({ status: "unavailable", code: "unsupported_host", @@ -333,6 +341,46 @@ describe("Ghidra platform support", () => { code: "architecture_unsupported", }); }); + it("admits generated PE32 applications while rejecting unsupported x86 roles", async () => { + const root = fileURLToPath(new URL("../../", import.meta.url)); + await promisify(execFile)(process.execPath, [ + join(root, "scripts/create-ghidra-windows-fixture.mjs"), + ]); + const path = join(root, "build/fixtures/rea-ghidra-windows-x86.exe"); + const bytes = await readFile(path); + const metadata = parseExecutableHeader(bytes, "x64"); + expect(metadata).toMatchObject({ + ok: true, + value: { + format: "pe", + architecture: "x86", + executableRole: "application", + managed: false, + }, + }); + if (!metadata.ok) throw new Error(metadata.error); + if (metadata.value.format !== "pe") + throw new Error("Expected a PE fixture"); + const target: BinaryTarget = { + path, + sha256: createHash("sha256").update(bytes).digest("hex"), + kind: "executable", + ...metadata.value, + }; + const ghidra = provider({ ...installationHost(), platform: "win32" }); + expect(ghidra.inspectTargetSupport(target).status).toBe("supported"); + for (const [candidate, code] of [ + [ + { ...target, executableRole: "shared-library" }, + "target_role_unsupported", + ], + [{ ...target, managed: true }, "managed_target_unsupported"], + ] as const) + expect(ghidra.inspectTargetSupport(candidate)).toMatchObject({ + status: "unsupported", + code, + }); + }); }); describe("Ghidra client projection", () => { diff --git a/src/ghidra/GhidraProvider.test.ts b/src/ghidra/GhidraProvider.test.ts deleted file mode 100644 index c38e7ed75b..0000000000 --- a/src/ghidra/GhidraProvider.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { parseConfig } from "../config.js"; -import { silentLogger } from "../logger.js"; -import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js"; -import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js"; -import { GhidraProvider } from "./GhidraProvider.js"; - -describe("Ghidra provider capabilities", () => { - it("publishes admitted operation effects and resists caller mutation", () => { - const config = parseConfig({}); - expect(config.ok).toBe(true); - if (!config.ok) return; - const provider = new GhidraProvider(config.value, silentLogger, { - platform: "linux", - architecture: "x64", - readText: () => undefined, - executable: () => false, - probeJava: () => undefined, - }); - const capabilities = provider.capabilities(); - const published = structuredClone(capabilities); - expect(capabilities.map(({ operation }) => operation).sort()).toEqual( - [...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].sort(), - ); - for (const descriptor of capabilities) { - expect(descriptor).toMatchObject({ - available: true, - reason: null, - effects: { - mutatesArtifact: descriptor.operation === "annotate_native_function", - mayShowUi: false, - mayAccessNetwork: false, - changesPermissions: false, - requiresRoot: false, - }, - }); - } - const baseLimitations = capabilities.find( - (item) => item.operation === "address_name", - )?.limitations; - for (const operation of ["search_procedures", "search_strings"]) { - expect( - capabilities.find((item) => item.operation === operation)?.limitations, - ).toEqual(baseLimitations); - } - const first = capabilities[0]; - if (first === undefined) throw new Error("Ghidra capabilities are empty"); - Reflect.set(capabilities, 0, { ...first, available: false }); - Reflect.set(first, "available", false); - Reflect.set(first.effects, "mutatesArtifact", true); - Reflect.set(first.limitations, 0, "forged limitation"); - - expect(provider.capabilities()).toEqual(published); - }); -}); diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index 17ef5cf202..008a3808cf 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -233,11 +233,11 @@ const inspectWindowsP0TargetSupport = ( reason: "Windows Ghidra P0 accepts PE targets only.", diagnostics, }; - if (target.architecture !== "x86_64") + if (target.architecture !== "x86" && target.architecture !== "x86_64") return { status: "unsupported", code: "architecture_unsupported", - reason: "Windows Ghidra P0 accepts x86-64 PE targets only.", + reason: "Windows Ghidra P0 accepts x86 and x86-64 PE targets only.", diagnostics, }; if (target.executableRole !== "application") diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts index 0bec2afa04..402f6d1e89 100644 --- a/src/ghidra/GhidraProviderCapabilities.ts +++ b/src/ghidra/GhidraProviderCapabilities.ts @@ -30,7 +30,7 @@ export const healthLimitations = Object.freeze([ /** Additional limitations applied to the experimental Windows x64 P0 boundary. */ export const windowsP0Limitations = Object.freeze([ - "Windows Ghidra P0 accepts approved native x86-64 PE applications only; DLL, managed, hostile, sensitive, and mutable-path targets are unsupported.", + "Windows Ghidra P0 accepts approved native x86 and x86-64 PE applications only; DLL, managed, hostile, sensitive, and mutable-path targets are unsupported.", "The Windows bridge uses authenticated IPv4 loopback because Node path-based IPC does not expose Java AF_UNIX sockets; the endpoint file contains no bearer token.", "Windows sessions require the matching packaged Windows x64 native addon, local NTFS targets and runtimes, and Windows 10 or later. Native handles enforce path admission, private DACLs, and Job Object ownership automatically.", ]); diff --git a/src/ghidra/GhidraResponseBuffer.test.ts b/src/ghidra/GhidraResponseBuffer.test.ts deleted file mode 100644 index ccae6d051d..0000000000 --- a/src/ghidra/GhidraResponseBuffer.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { GhidraResponseBuffer } from "./GhidraResponseBuffer.js"; - -describe("Ghidra response buffer", () => { - it("preserves fragmented lines, whitespace and a trailing partial response", () => { - const lines: string[] = []; - const buffer = new GhidraResponseBuffer({ - onLine: (line) => lines.push(line), - }); - buffer.push(' {"result":"雪'); - buffer.push('🦊"}\r'); - buffer.push('\n\n {"result":2}\n{"res'); - expect(lines).toEqual(['{"result":"雪🦊"}', '{"result":2}']); - buffer.push('ult":3}'); - expect(lines).toHaveLength(2); - buffer.push("\n"); - expect(lines).toEqual([ - '{"result":"雪🦊"}', - '{"result":2}', - '{"result":3}', - ]); - }); - - it("drops both pending fragments and remaining lines when reset during delivery", () => { - const lines: string[] = []; - const buffer = new GhidraResponseBuffer({ - onLine: (line) => { - lines.push(line); - buffer.reset(); - }, - }); - buffer.push("discarded partial"); - buffer.reset(); - buffer.push("first\nsecond\npartial"); - buffer.push("discarded"); - buffer.reset(); - buffer.push("next\n"); - expect(lines).toEqual(["first", "next"]); - }); - - it("accepts a large response arriving in many small socket fragments", () => { - const lines: string[] = []; - const buffer = new GhidraResponseBuffer({ - onLine: (line) => lines.push(line), - }); - const response = `{"result":"${"x".repeat(8 * 1024 * 1024)}"}`; - for (let offset = 0; offset < response.length; offset += 4096) - buffer.push(response.slice(offset, offset + 4096)); - expect(lines).toEqual([]); - buffer.push("\n"); - expect(lines).toEqual([response]); - }); - - it("accepts complete responses larger than the former 64 MiB ceiling", () => { - const lines: string[] = []; - const buffer = new GhidraResponseBuffer({ - onLine: (line) => lines.push(line), - }); - const response = `{"result":"${"x".repeat(64 * 1024 * 1024 + 1)}"}`; - - buffer.push(`${response}\n`); - - expect(lines).toEqual([response]); - }); -}); diff --git a/src/hopper/BridgeLauncher.ts b/src/hopper/BridgeLauncher.ts index efa5563f5e..af60844e2b 100644 --- a/src/hopper/BridgeLauncher.ts +++ b/src/hopper/BridgeLauncher.ts @@ -336,6 +336,7 @@ const launchHopperProcess = async (input: { arguments: arguments_, runId: input.session.runId, expectedCommand: ownershipCommand, + ...(input.signal === undefined ? {} : { signal: input.signal }), }); if (input.linuxDemo !== undefined) return spawnOwnedProviderProcess({ @@ -343,6 +344,7 @@ const launchHopperProcess = async (input: { arguments: input.linuxDemo.args, runId: input.session.runId, expectedCommand: ownershipCommand, + ...(input.signal === undefined ? {} : { signal: input.signal }), }); return spawn(input.argumentsForTarget); }; diff --git a/src/hopper/HopperProvider.test.ts b/src/hopper/HopperProvider.test.ts index 2fdb8e06ec..8b08582e91 100644 --- a/src/hopper/HopperProvider.test.ts +++ b/src/hopper/HopperProvider.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { parseConfig } from "../config.js"; import { silentLogger } from "../logger.js"; -import { HopperProvider, HOPPER_OPERATIONS } from "./HopperProvider.js"; +import { HopperProvider } from "./HopperProvider.js"; describe("Hopper provider capabilities", () => { it("declines DOS MZ targets with a provider-specific support reason", () => { @@ -24,42 +24,4 @@ describe("Hopper provider capabilities", () => { reason: expect.stringContaining("Ghidra"), }); }); - - it("publishes deterministic descriptors and resists caller mutation", () => { - const config = parseConfig({}); - expect(config.ok).toBe(true); - if (!config.ok) throw new Error("expected valid configuration"); - const provider = new HopperProvider(config.value, silentLogger); - const capabilities = provider.capabilities(); - const published = structuredClone(capabilities); - expect(capabilities.map(({ operation }) => operation)).toEqual([ - ...HOPPER_OPERATIONS, - ]); - expect(new Set(capabilities.map(({ operation }) => operation)).size).toBe( - capabilities.length, - ); - for (const descriptor of capabilities) { - expect(descriptor.provider).toEqual(provider.identity()); - expect(descriptor).toMatchObject({ - available: true, - reason: null, - }); - } - expect( - capabilities.find(({ operation }) => operation === "list_procedures"), - ).toMatchObject({ available: true }); - expect( - capabilities.find(({ operation }) => operation === "set_comment"), - ).toMatchObject({ - effects: { mutatesArtifact: true, mayWriteFilesystem: true }, - }); - const first = capabilities[0]; - if (first === undefined) throw new Error("Hopper capabilities are empty"); - Reflect.set(capabilities, 0, { ...first, available: false }); - Reflect.set(first, "available", false); - Reflect.set(first.effects, "mutatesArtifact", true); - Reflect.set(first.limitations, 0, "forged limitation"); - - expect(provider.capabilities()).toEqual(published); - }); }); diff --git a/src/hopper/HopperResponseStream.test.ts b/src/hopper/HopperResponseStream.test.ts deleted file mode 100644 index fe3bd7ab63..0000000000 --- a/src/hopper/HopperResponseStream.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { HopperResponseStream } from "./HopperResponseStream.js"; - -describe("Hopper response stream", () => { - it("preserves fragmented messages and drops partial bytes on reset", () => { - const messages: unknown[] = []; - const stream = new HopperResponseStream({ - accept: (message) => { - messages.push(message); - return true; - }, - hasQueued: () => false, - nextRequestId: () => 4, - abort: (message) => { - throw new Error(message); - }, - }); - stream.push("discarded"); - stream.reset(); - stream.push(' {"id":1,"result":"雪'); - stream.push('🦊"}\r'); - stream.push('\n\n{"id":2,"result":null}\n{"id":'); - stream.push('3,"result":true}'); - expect(messages).toEqual([ - { id: 1, result: "雪🦊" }, - { id: 2, result: null }, - ]); - stream.push("\n"); - expect(messages).toEqual([ - { id: 1, result: "雪🦊" }, - { id: 2, result: null }, - { id: 3, result: true }, - ]); - }); - - it("stops delivering queued lines when validation aborts and resets the stream", () => { - const messages: unknown[] = []; - const failures: string[] = []; - const stream = new HopperResponseStream({ - accept: (message) => { - messages.push(message); - return true; - }, - hasQueued: () => false, - nextRequestId: () => 2, - abort: (message) => { - failures.push(message); - stream.reset(); - }, - }); - stream.push('malformed\n{"id":1,"result":true}\n'); - expect(messages).toEqual([]); - expect(failures).toHaveLength(1); - stream.push('{"id":1,"result":false}\n'); - expect(messages).toEqual([{ id: 1, result: false }]); - }); - - it("accepts a large response arriving in many small socket fragments", () => { - const messages: unknown[] = []; - const stream = new HopperResponseStream({ - accept: (message) => { - messages.push(message); - return true; - }, - hasQueued: () => false, - nextRequestId: () => 2, - abort: (message) => { - throw new Error(message); - }, - }); - const response = `{"id":1,"result":"${"x".repeat(8 * 1024 * 1024)}"}`; - for (let offset = 0; offset < response.length; offset += 4096) - stream.push(response.slice(offset, offset + 4096)); - expect(messages).toEqual([]); - stream.push("\n"); - expect(messages).toEqual([JSON.parse(response)]); - }); - - it("accepts complete responses larger than the former 10 MiB ceiling", () => { - const messages: unknown[] = []; - const failures: string[] = []; - const stream = new HopperResponseStream({ - accept: (message) => { - messages.push(message); - return true; - }, - hasQueued: () => false, - nextRequestId: () => 2, - abort: (message) => failures.push(message), - }); - const line = `{"id":1,"result":"${"x".repeat(10 * 1024 * 1024 + 1)}"}`; - - stream.push(`${line}\n`); - - expect(failures).toEqual([]); - expect(messages).toEqual([JSON.parse(line)]); - }); -}); diff --git a/src/hopper/LinuxPrivateDisplayProbe.ts b/src/hopper/LinuxPrivateDisplayProbe.ts index 1ec6190d08..27f136185b 100644 --- a/src/hopper/LinuxPrivateDisplayProbe.ts +++ b/src/hopper/LinuxPrivateDisplayProbe.ts @@ -20,6 +20,9 @@ const PYTHON_PATH = "/usr/bin/python3"; const UNSHARE_PATH = "/usr/bin/unshare"; const DEFAULT_PROBE_TIMEOUT_MS = 5_000; +const isAborted = (signal: AbortSignal | undefined): boolean => + signal?.aborted === true; + export type LinuxPrivateDisplayRunnableStrategy = Exclude< HopperPrivateDisplayStrategy, "unavailable" @@ -101,8 +104,7 @@ export const selectLinuxPrivateDisplayStrategy = async (options: { /** Run one helper probe in an owned process group and retain its full diagnostics. */ export const runLinuxPrivateDisplayProbe: LinuxPrivateDisplayProbeRunner = async (strategy, options) => { - if (options.signal?.aborted === true) - return emptyProcessResult("cancelled"); + if (isAborted(options.signal)) return emptyProcessResult("cancelled"); const command = privateDisplayProbeCommand(options.helperPath, strategy); let started: Awaited>; try { @@ -111,11 +113,14 @@ export const runLinuxPrivateDisplayProbe: LinuxPrivateDisplayProbeRunner = arguments: command.arguments, runId: randomUUID(), expectedCommand: null, + ...(options.signal === undefined ? {} : { signal: options.signal }), }); } catch (cause: unknown) { // best-effort cleanup: probe spawn failure is reported as launch-failed. void cause; - return emptyProcessResult("launch-failed"); + return emptyProcessResult( + isAborted(options.signal) ? "cancelled" : "launch-failed", + ); } const launch: ProviderProcessLaunch = { process: started.process, diff --git a/src/ida/IdaProviderCapabilities.test.ts b/src/ida/IdaProviderCapabilities.test.ts deleted file mode 100644 index cc36a62b3c..0000000000 --- a/src/ida/IdaProviderCapabilities.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect, it } from "vitest"; - -import { IDA_OPERATIONS, idaCapabilities } from "./IdaProviderCapabilities.js"; - -it("preserves live read-only capabilities when callers attempt to alter declarations", () => { - const original = idaCapabilities("headless", true); - Reflect.set(IDA_OPERATIONS, 0, "set_comment"); - Reflect.set(IDA_OPERATIONS, IDA_OPERATIONS.length, "set_address_name"); - - expect(idaCapabilities("headless", true)).toEqual(original); - expect(original.every(({ cachePolicy }) => cachePolicy === "live")).toBe( - true, - ); - expect(original.every(({ effects }) => !effects.mutatesArtifact)).toBe(true); - expect( - original.find(({ operation }) => operation === "procedure_callers"), - ).toMatchObject({ available: false }); -}); diff --git a/src/inspector/JavaScriptRuntimeScope.test.ts b/src/inspector/JavaScriptRuntimeScope.test.ts deleted file mode 100644 index caa876dee7..0000000000 --- a/src/inspector/JavaScriptRuntimeScope.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - authorizeRuntimeLocation, - inspectorExclusionKey, -} from "./JavaScriptRuntimeScope.js"; - -describe("runtime location authorization", () => { - it("reports canonical exclusion reasons per denial case", async () => { - expect(await authorizeRuntimeLocation("not a url")).toEqual({ - allowed: false, - reason: "unsupported_url", - }); - expect(await authorizeRuntimeLocation("ftp://example.test/app.js")).toEqual( - { - allowed: false, - reason: "unsupported_url", - }, - ); - expect( - await authorizeRuntimeLocation("file:///outside-approved-scope.js"), - ).toEqual({ allowed: false, reason: "not_approved" }); - }); - - it("collapses every canonical denial onto the fixed wire bucket", () => { - expect(inspectorExclusionKey("unsupported_url")).toBe( - "unsupported_location", - ); - expect(inspectorExclusionKey("not_approved")).toBe("unsupported_location"); - expect(inspectorExclusionKey("disallowed_origin")).toBe( - "unsupported_location", - ); - }); -}); diff --git a/src/inspector/V8InspectorCaptureProjection.test.ts b/src/inspector/V8InspectorCaptureProjection.test.ts new file mode 100644 index 0000000000..124745e67a --- /dev/null +++ b/src/inspector/V8InspectorCaptureProjection.test.ts @@ -0,0 +1,216 @@ +import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { afterAll, beforeAll, expect, it } from "vitest"; + +import { finalizeInspectorCapture } from "./V8InspectorCaptureProjection.js"; +import type { CaptureState } from "./V8InspectorProvider.js"; + +let root: string; +beforeAll(async () => { + root = await mkdtemp(join(tmpdir(), "rea-inspector-projection-")); +}); +afterAll(async () => { + if (root !== undefined) await rm(root, { recursive: true, force: true }); +}); + +const target = { + id: "target-1", + type: "node", + url: "file:///tmp/target.js", + attached: false, + webSocketUrl: "ws://127.0.0.1:9222/target-1", + location: { kind: "file" as const, file_path: "/tmp/target.js" }, +}; +const stateFor = (scripts: CaptureState["scripts"]): CaptureState => ({ + scripts, + contexts: new Map(), + eventsObserved: scripts.length, + eventsRetained: scripts.length, + eventsDropped: 0, + metadataBytes: 0, + scriptsObserved: scripts.length, + invalidScripts: 0, + truncated: false, + truncationReasons: new Set(), +}); +const finalize = ( + state: CaptureState, + authorizeLocation?: Parameters< + typeof finalizeInspectorCapture + >[0]["authorizeLocation"], +) => + finalizeInspectorCapture({ + input: { + inspector_endpoint: "http://127.0.0.1:9222", + target_id: target.id, + observation_ms: 100, + }, + runtime: { + product: "Node.js/v24.18.0", + protocol_version: "1.3", + v8_version: null, + }, + target, + state, + ...(authorizeLocation === undefined ? {} : { authorizeLocation }), + }); + +it("authorizes distinct Inspector locations and preserves stable deduplication", async () => { + const firstPath = join(root, "first.js"); + const secondPath = join(root, "second.js"); + await Promise.all([ + writeFile(firstPath, "void 0;\n"), + writeFile(secondPath, "void 1;\n"), + ]); + const firstUrl = pathToFileURL(firstPath).href; + const secondUrl = pathToFileURL(secondPath).href; + const result = await finalize( + stateFor([ + { + rawUrl: firstUrl, + executionContextKey: "1", + cdpHash: "first", + length: 7, + isModule: false, + }, + { + rawUrl: firstUrl, + executionContextKey: "1", + cdpHash: "first", + length: 7, + isModule: false, + }, + { + rawUrl: secondUrl, + executionContextKey: "1", + cdpHash: "second", + length: 7, + isModule: false, + }, + { + rawUrl: "ftp://example.test/unapproved.js", + executionContextKey: null, + cdpHash: null, + length: 0, + isModule: false, + }, + ]), + ); + + expect(result.scripts.items.map(({ location }) => location)).toEqual( + expect.arrayContaining([ + { kind: "file", file_path: await realpath(firstPath) }, + { kind: "file", file_path: await realpath(secondPath) }, + ]), + ); + expect(result.scripts.items).toHaveLength(2); + expect(result.scripts.excluded).toEqual({ + unsupported_location: 1, + invalid_protocol_value: 0, + }); +}); + +it("authorizes each unique script location through a bounded worker pool", async () => { + const drafts = Array.from({ length: 200 }, (_, index) => ({ + rawUrl: `file:///fixture/script-${String(index)}.js`, + executionContextKey: "1", + cdpHash: `hash-${String(index)}`, + length: 7, + isModule: false, + })); + const state = stateFor([ + ...drafts, + ...drafts.slice(0, 1), + { + rawUrl: "ftp://example.test/unapproved.js", + executionContextKey: null, + cdpHash: null, + length: 0, + isModule: false, + }, + ]); + const activeUrls = new Set(); + const authorizedUrls = new Set(); + let maximumActive = 0; + const result = await finalize(state, async (rawUrl) => { + activeUrls.add(rawUrl); + authorizedUrls.add(rawUrl); + maximumActive = Math.max(maximumActive, activeUrls.size); + await new Promise((resolve) => setTimeout(resolve, 1)); + activeUrls.delete(rawUrl); + return rawUrl.startsWith("ftp:") + ? { allowed: false, reason: "unsupported_url" } + : { allowed: true, location: { kind: "file", file_path: rawUrl } }; + }); + + expect(maximumActive).toBeGreaterThan(1); + expect(maximumActive).toBeLessThan(authorizedUrls.size); + expect(authorizedUrls.size).toBe(201); + expect(activeUrls.size).toBe(0); + expect(result.scripts.items).toHaveLength(200); + expect(result.scripts.observed_total).toBe(202); + expect(result.scripts.excluded).toEqual({ + unsupported_location: 1, + invalid_protocol_value: 0, + }); +}); + +it("waits for active authorization workers to settle before returning a failure", async () => { + const scripts = Array.from({ length: 12 }, (_, index) => ({ + rawUrl: `file:///fixture/failure-${String(index)}.js`, + executionContextKey: null, + cdpHash: null, + length: 0, + isModule: false, + })); + const startedUrls: string[] = []; + const pending = new Map< + string, + { readonly resolve: () => void; readonly reject: (error: Error) => void } + >(); + const failure = new Error("authorization failed"); + let active = 0; + let maximumActive = 0; + let failureInjected = false; + + const finalization = finalize(stateFor(scripts), async (rawUrl) => { + startedUrls.push(rawUrl); + if (failureInjected) + return { allowed: true, location: { kind: "file", file_path: rawUrl } }; + active += 1; + maximumActive = Math.max(maximumActive, active); + return new Promise<{ + readonly allowed: true; + readonly location: { readonly kind: "file"; readonly file_path: string }; + }>((resolve, reject) => { + pending.set(rawUrl, { + resolve: () => + resolve({ + allowed: true, + location: { kind: "file", file_path: rawUrl }, + }), + reject, + }); + }).finally(() => { + active -= 1; + }); + }); + const initialStarted = new Set(startedUrls); + expect(initialStarted.size).toBeGreaterThan(1); + expect(maximumActive).toBe(initialStarted.size); + const failingUrl = startedUrls[0]; + if (failingUrl === undefined) + throw new Error("Expected at least one active authorization"); + failureInjected = true; + pending.get(failingUrl)?.reject(failure); + await Promise.resolve(); + expect(new Set(startedUrls)).toEqual(initialStarted); + for (const [rawUrl, request] of pending) + if (rawUrl !== failingUrl) request.resolve(); + await expect(finalization).rejects.toBe(failure); + expect(new Set(startedUrls)).toEqual(initialStarted); + expect(active).toBe(0); +}); diff --git a/src/inspector/V8InspectorCaptureProjection.ts b/src/inspector/V8InspectorCaptureProjection.ts index 84d1e00afd..102247afb8 100644 --- a/src/inspector/V8InspectorCaptureProjection.ts +++ b/src/inspector/V8InspectorCaptureProjection.ts @@ -38,6 +38,14 @@ interface FinalizeCaptureInput { readonly runtime: JavaScriptRuntimeTargetList["runtime"]; readonly target: AuthorizedV8InspectorTarget; readonly state: CaptureState; + readonly authorizeLocation?: typeof authorizeRuntimeLocation; +} + +const LOCATION_AUTHORIZATION_WORKERS = 8; + +interface AuthorizedLocationGroup { + readonly drafts: readonly ScriptDraft[]; + readonly decision: Awaited>; } /** Canonically authorize, deduplicate, and sort one bounded raw capture. */ @@ -46,20 +54,61 @@ export const finalizeInspectorCapture = async ({ runtime, target, state, + authorizeLocation = authorizeRuntimeLocation, }: FinalizeCaptureInput): Promise => { const exclusions = createInspectorExclusionCounts(); const scripts = new Map< string, JavaScriptRuntimeObservation["scripts"]["items"][number] >(); + const draftsByUrl = new Map(); for (const draft of state.scripts) { - const decision = await authorizeRuntimeLocation(draft.rawUrl); - if (!decision.allowed) { - exclusions[inspectorExclusionKey(decision.reason)] += 1; - continue; + const drafts = draftsByUrl.get(draft.rawUrl) ?? []; + drafts.push(draft); + draftsByUrl.set(draft.rawUrl, drafts); + } + const groups = [...draftsByUrl]; + const authorizedGroups: AuthorizedLocationGroup[] = new Array(groups.length); + let nextGroup = 0; + let stopScheduling = false; + const worker = async (): Promise => { + try { + for (;;) { + if (stopScheduling) return; + const index = nextGroup; + nextGroup += 1; + const group = groups[index]; + if (group === undefined) return; + const [rawUrl, drafts] = group; + authorizedGroups[index] = { + drafts, + decision: await authorizeLocation(rawUrl), + }; + } + } catch (cause: unknown) { + stopScheduling = true; + throw cause; + } + }; + const workerResults = await Promise.allSettled( + Array.from( + { length: Math.min(LOCATION_AUTHORIZATION_WORKERS, groups.length) }, + worker, + ), + ); + const failedWorker = workerResults.find( + (result) => result.status === "rejected", + ); + if (failedWorker?.status === "rejected") throw failedWorker.reason; + for (const { drafts, decision } of authorizedGroups) { + for (const draft of drafts) { + if (!decision.allowed) { + exclusions[inspectorExclusionKey(decision.reason)] += 1; + continue; + } + const script = scriptFromDraft(draft, decision.location); + scripts.set(script.script_key, script); } - const script = scriptFromDraft(draft, decision.location); - scripts.set(script.script_key, script); } const items = [...scripts.values()].sort((left, right) => left.script_key < right.script_key diff --git a/src/javascript/recovery/WakaruCommand.ts b/src/javascript/recovery/WakaruCommand.ts index ad90338ef2..42062feeed 100644 --- a/src/javascript/recovery/WakaruCommand.ts +++ b/src/javascript/recovery/WakaruCommand.ts @@ -101,8 +101,11 @@ export const runWakaruCommand = async (context: WakaruCommandContext) => { runId, cwd: context.cwd, hostEnvironment: context.environment, + ...(context.signal === undefined ? {} : { signal: context.signal }), env: { TMPDIR: context.cwd, RAYON_NUM_THREADS: "1", NO_COLOR: "1" }, }).catch((cause: unknown) => { + if (context.signal?.aborted === true) + throw new AnalysisCancelledError(OPERATION); throw new ProviderAdapterError("wakaru", OPERATION, { cause, diagnostics: { diff --git a/src/javascript/sourceMaps/SourceMapCommand.ts b/src/javascript/sourceMaps/SourceMapCommand.ts index 62df4142c2..eec1f88447 100644 --- a/src/javascript/sourceMaps/SourceMapCommand.ts +++ b/src/javascript/sourceMaps/SourceMapCommand.ts @@ -120,6 +120,7 @@ class OwnedCodec { runId: this.#runId, cwd: this.#runtime.path, hostEnvironment: environment, + signal: this.deadline.signal, }); this.#supervisor = new ProviderProcessSupervisor( { diff --git a/src/native/CommandRunner.ts b/src/native/CommandRunner.ts index be6329e031..962535fa8c 100644 --- a/src/native/CommandRunner.ts +++ b/src/native/CommandRunner.ts @@ -124,7 +124,8 @@ const ALLOWED_TOOLS = new Set([ "vtool", ]); -const resolveXcrunTool = async ( +/** Locate an Xcode tool through `xcrun --find` and pin its executable digest. */ +export const resolveXcrunTool = async ( tool: string, signal?: AbortSignal, ): Promise> => { diff --git a/src/native/LldbCallTracer.ts b/src/native/LldbCallTracer.ts new file mode 100644 index 0000000000..31b14731ea --- /dev/null +++ b/src/native/LldbCallTracer.ts @@ -0,0 +1,400 @@ +import { spawn } from "node:child_process"; +import { open, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { z } from "zod"; + +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisTimeoutError, +} from "../domain/analysisErrorCore.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { + nativeCallEventSchema, + nativeCodeLocationSchema, + type NativeCallObservationInput, +} from "../domain/native/nativeCallObservation.js"; +import { ProviderAdapterError } from "../domain/providerAdapterError.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { safeParseJson } from "../domain/safeJson.js"; +import { execFileOutput } from "../process/ExecFileOutput.js"; +import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; +import { resolveXcrunTool, type ResolvedTool } from "./CommandRunner.js"; + +const OPERATION = "observe_native_calls"; +const PROVIDER = "native-macos"; +/** Captured target output kept per stream; the rest is counted, not stored. */ +const MAX_OUTPUT_BYTES = 1024 * 1024; +/** LLDB startup, symbol loading and teardown beyond the observation window. */ +const DEADLINE_GRACE_MS = 60_000; +const DIAGNOSTIC_BYTES = 16 * 1024; + +const BRIDGE = fileURLToPath( + new URL("../../bridge/native/rea_lldb_tracer.py", import.meta.url), +); + +/** JSON the LLDB bridge writes after a completed run. */ +const tracedSchema = z.strictObject({ + status: z.literal("traced"), + version: z.string(), + pid: z.number().int().positive(), + outcome: z.enum(["exited", "duration-elapsed", "event-limit", "stop-limit"]), + exit_status: z.number().int().nullable(), + exit_description: z.string().nullable(), + killed: z.boolean(), + terminated: z.boolean(), + elapsed_ms: z.number().nonnegative(), + breakpoints: z.array( + z.strictObject({ + index: z.number().int().nonnegative(), + location_count: z.number().int().nonnegative(), + locations: z.array(nativeCodeLocationSchema), + }), + ), + events: z.array(nativeCallEventSchema), + other_stops: z.array(z.string()), +}); + +const tracerOutputSchema = z.discriminatedUnion("status", [ + tracedSchema, + z.strictObject({ + status: z.enum(["target-error", "launch-error", "tracer-error"]), + error: z.string().nullable(), + }), +]); + +/** Bounded text of one target output stream. */ +export interface CapturedOutput { + readonly text: string; + readonly bytes: number; + readonly truncated: boolean; +} + +/** One completed LLDB observation, before projection into the result contract. */ +export interface NativeCallTrace { + readonly debugger: ResolvedTool & { readonly version: string | null }; + readonly run: z.infer; + readonly stdout: CapturedOutput; + readonly stderr: CapturedOutput; + /** REA confirmed the traced process no longer exists. */ + readonly terminated: boolean; +} + +/** Launch one owned process under a debugger and record calls. */ +export interface NativeCallTracer { + trace( + request: { + readonly executable: string; + readonly architecture: string; + readonly input: NativeCallObservationInput; + }, + signal?: AbortSignal, + ): Promise>; +} + +/** Remediation for a target LLDB may not debug. */ +const ATTACH_REMEDIATION = + "Allow debugging for this user (Developer Tools access: `DevToolsSecurity -enable`, or membership in the _developer group). A hardened-runtime target is debuggable only with the com.apple.security.get-task-allow entitlement; see inspect_signature's debugger-attach facet."; + +/** Production tracer: `lldb --batch` running the REA LLDB bridge. */ +export class LldbCallTracer implements NativeCallTracer { + async trace( + request: Parameters[0], + signal?: AbortSignal, + ): Promise> { + const lldb = await resolveXcrunTool("lldb", signal); + if (!lldb.ok) + return err( + lldb.error.reason === "cancelled" + ? new AnalysisCancelledError(OPERATION) + : new AnalysisCapabilityUnavailableError( + PROVIDER, + OPERATION, + "LLDB from Xcode or the Command Line Tools is not available", + ), + ); + const root = await PrivateRuntimeRoot.create({ prefix: "rea-lldb-" }); + try { + return await this.#run(lldb.value, root.path, request, signal); + } finally { + await root.close(); + } + } + + async #run( + lldb: ResolvedTool, + directory: string, + request: Parameters[0], + signal?: AbortSignal, + ): Promise> { + const paths = { + config: join(directory, "config.json"), + result: join(directory, "result.json"), + pid: join(directory, "pid"), + stdout: join(directory, "stdout"), + stderr: join(directory, "stderr"), + }; + const { input } = request; + await writeFile( + paths.config, + JSON.stringify({ + executable: request.executable, + architecture: request.architecture, + arguments: input.arguments, + environment: input.environment, + working_directory: input.working_directory ?? null, + breakpoints: input.breakpoints.map((breakpoint) => + breakpoint.kind === "function" + ? { ...breakpoint, module: breakpoint.module ?? null } + : { ...breakpoint, class_name: breakpoint.class_name ?? null }, + ), + duration_ms: input.duration_ms, + max_events: input.max_events, + argument_registers: input.argument_registers, + backtrace_frames: input.backtrace_frames, + stdout_path: paths.stdout, + stderr_path: paths.stderr, + result_path: paths.result, + pid_path: paths.pid, + }), + { mode: 0o600 }, + ); + const exited = await runLldb( + lldb.path, + [ + "--batch", + "--no-lldbinit", + "--no-use-colors", + "-o", + `command script import ${quote(BRIDGE)}`, + "-o", + `rea_trace ${paths.config}`, + ], + { + deadlineMs: input.duration_ms + DEADLINE_GRACE_MS, + ...(signal === undefined ? {} : { signal }), + }, + ); + const terminated = await ensureTerminated(paths.pid, request.executable); + if (exited.kind === "cancelled") + return err(new AnalysisCancelledError(OPERATION)); + if (exited.kind === "timeout") + return err( + new AnalysisTimeoutError( + OPERATION, + input.duration_ms + DEADLINE_GRACE_MS, + ), + ); + const output = await readTracerOutput(paths.result); + if (output === undefined) + return err( + new ProviderAdapterError(PROVIDER, OPERATION, { + diagnostics: { + reason: "LLDB exited without a tracer result", + exit_code: exited.exitCode, + lldb_output: exited.output, + }, + }), + ); + if (output.status !== "traced") return err(tracerFailure(output)); + return ok({ + debugger: { ...lldb, version: output.version }, + run: output, + stdout: await capturedOutput(paths.stdout), + stderr: await capturedOutput(paths.stderr), + terminated, + }); + } +} + +/** LLDB command arguments are quoted; the bridge path is REA's own install path. */ +const quote = (value: string): string => + `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; + +const tracerFailure = (output: { + readonly status: "target-error" | "launch-error" | "tracer-error"; + readonly error: string | null; +}): AnalysisError => { + const message = output.error ?? "no LLDB error text"; + if (output.status === "tracer-error") + return new ProviderAdapterError(PROVIDER, OPERATION, { + diagnostics: { reason: `LLDB bridge failed: ${message}` }, + }); + if (output.status === "target-error") + return new AnalysisCapabilityUnavailableError( + PROVIDER, + OPERATION, + `lldb-target-unloadable: ${message}`, + ); + // debugserver reports denied task access as an attach failure. + return /attach|not allowed|permission|denied|debugserver/iu.test(message) + ? new AnalysisCapabilityUnavailableError( + PROVIDER, + OPERATION, + `debugger-attach-denied: ${message}`, + { userMessage: ATTACH_REMEDIATION }, + ) + : new AnalysisCapabilityUnavailableError( + PROVIDER, + OPERATION, + `launch-failed: ${message}`, + ); +}; + +const readTracerOutput = async ( + path: string, +): Promise | undefined> => { + let text: string; + try { + text = await readFile(path, "utf8"); + } catch { + return undefined; + } + const parsed = safeParseJson(text); + if (!parsed.ok) return undefined; + const output = tracerOutputSchema.safeParse(parsed.value); + return output.success ? output.data : undefined; +}; + +const capturedOutput = async (path: string): Promise => { + let handle; + try { + handle = await open(path, "r"); + } catch { + return { text: "", bytes: 0, truncated: false }; + } + try { + const { size } = await handle.stat(); + const buffer = Buffer.alloc(Math.min(size, MAX_OUTPUT_BYTES)); + const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0); + return { + text: buffer.subarray(0, bytesRead).toString("utf8"), + bytes: size, + truncated: size > MAX_OUTPUT_BYTES, + }; + } finally { + await handle.close(); + } +}; + +type LldbExit = + | { + readonly kind: "exited"; + readonly exitCode: number | null; + readonly output: string; + } + | { readonly kind: "cancelled" } + | { readonly kind: "timeout" }; + +/** Run LLDB in its own process group so cancellation can stop it and debugserver. */ +const runLldb = ( + executable: string, + arguments_: readonly string[], + options: { readonly signal?: AbortSignal; readonly deadlineMs: number }, +): Promise => + new Promise((resolve) => { + if (options.signal?.aborted === true) { + resolve({ kind: "cancelled" }); + return; + } + const child = spawn(executable, [...arguments_], { + shell: false, + detached: true, + stdio: ["ignore", "pipe", "pipe"], + }); + let output = ""; + let stopped: "cancelled" | "timeout" | undefined; + const keep = (chunk: Buffer): void => { + output = (output + chunk.toString("utf8")).slice(-DIAGNOSTIC_BYTES); + }; + child.stdout.on("data", keep); + child.stderr.on("data", keep); + const kill = (reason: "cancelled" | "timeout"): void => { + stopped ??= reason; + if (child.pid !== undefined) + try { + process.kill(-child.pid, "SIGKILL"); + } catch { + // The group already exited. + } + }; + const timer = setTimeout(() => { + kill("timeout"); + }, options.deadlineMs); + const onAbort = (): void => { + kill("cancelled"); + }; + options.signal?.addEventListener("abort", onAbort, { once: true }); + const finish = (exitCode: number | null): void => { + clearTimeout(timer); + options.signal?.removeEventListener("abort", onAbort); + resolve( + stopped === undefined + ? { kind: "exited", exitCode, output } + : { kind: stopped }, + ); + }; + child.on("error", () => { + finish(null); + }); + child.on("close", (code) => { + finish(code); + }); + }); + +/** + * debugserver kills the traced process when LLDB exits. Confirm it, and kill a + * survivor only when its executable is still the traced one. + */ +const ensureTerminated = async ( + pidPath: string, + executable: string, +): Promise => { + let pid: number; + try { + pid = Number.parseInt(await readFile(pidPath, "utf8"), 10); + } catch { + // LLDB never launched the target. + return true; + } + if (!Number.isSafeInteger(pid) || pid <= 0) return false; + for (let attempt = 0; attempt < 20; attempt++) { + if (!alive(pid)) return true; + if (attempt === 10 && (await commandOf(pid)) === executable) + try { + process.kill(pid, "SIGKILL"); + } catch { + // It exited between the checks. + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return !alive(pid); +}; + +const alive = (pid: number): boolean => { + try { + process.kill(pid, 0); + return true; + } catch (cause: unknown) { + return !( + cause instanceof Error && + "code" in cause && + cause.code === "ESRCH" + ); + } +}; + +const commandOf = async (pid: number): Promise => { + try { + const { stdout } = await execFileOutput( + "/bin/ps", + ["-o", "comm=", "-p", String(pid)], + { timeout: 5_000 }, + ); + return stdout.trim(); + } catch { + return undefined; + } +}; diff --git a/src/native/NativeCallObservation.ts b/src/native/NativeCallObservation.ts new file mode 100644 index 0000000000..747ee58a66 --- /dev/null +++ b/src/native/NativeCallObservation.ts @@ -0,0 +1,182 @@ +import { createHash } from "node:crypto"; +import { open } from "node:fs/promises"; + +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisInputError, +} from "../domain/analysisErrorCore.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { BinaryTarget } from "../domain/binaryTarget.js"; +import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; +import type { JsonValue } from "../domain/jsonValue.js"; +import { + nativeCallObservationInputSchema, + nativeCallObservationResultSchema, + type NativeCallObservationInput, + type NativeCallObservationResult, +} from "../domain/native/nativeCallObservation.js"; +import { err, ok, type Result } from "../domain/result.js"; +import type { NativeCallTrace, NativeCallTracer } from "./LldbCallTracer.js"; + +const OPERATION = "observe_native_calls"; + +/** What every call observation leaves unobserved or approximated. */ +export const NATIVE_CALL_OBSERVATION_LIMITATIONS = [ + "Only entries are observed; return values, floating-point and stack-passed arguments are not captured.", + "Registers are the raw integer argument registers at entry. For Objective-C methods the first two hold the receiver and the selector.", + "Receiver classes come from LLDB's Objective-C runtime reader without running target code; they are null for class methods and for receivers LLDB cannot classify.", + "Calls that never reach a symbol's entry, such as inlined code or objc_direct methods called directly, are not observed.", + "The target is launched directly under LLDB, not through LaunchServices; LLDB disables ASLR for it, so load addresses can equal file addresses.", + "The observation window is checked about once a second, and recording pauses the process at every entry, which slows it.", +] as const; + +/** Launch the active Mach-O under LLDB, record requested call entries, then stop it. */ +export const observeNativeCalls = async ( + target: BinaryTarget, + parameters: Readonly>, + tracer: NativeCallTracer, + signal?: AbortSignal, +): Promise> => { + if (target.kind !== "executable" || target.format !== "mach-o") + return err( + new AnalysisCapabilityUnavailableError( + "native-macos", + OPERATION, + "Native call observation launches a Mach-O executable; the active target is not one", + ), + ); + const parsed = nativeCallObservationInputSchema.safeParse(parameters); + if (!parsed.success) + return err(new AnalysisInputError(OPERATION, { cause: parsed.error })); + // Observe the bytes the session's other Evidence describes. + if ((await fileSha256(target.path, signal)) !== target.sha256) + return err( + new EvidenceIntegrityError( + "Native call observation target digest changed after session binding", + ), + ); + if (signal?.aborted === true) + return err(new AnalysisCancelledError(OPERATION)); + const traced = await tracer.trace( + { + executable: target.path, + architecture: target.architecture, + input: parsed.data, + }, + signal, + ); + if (!traced.ok) return traced; + return ok(projectNativeCalls(target, parsed.data, traced.value)); +}; + +/** Join the request, the tracer's run and REA's own checks into the result contract. */ +export const projectNativeCalls = ( + target: Pick & { + readonly architecture: string; + }, + input: NativeCallObservationInput, + trace: NativeCallTrace, +): NativeCallObservationResult => { + const { run } = trace; + const unresolved = run.breakpoints + .filter(({ location_count: count }) => count === 0) + .map(({ index }) => index); + const eventLimit = run.outcome === "event-limit"; + const partial = + eventLimit || run.outcome === "stop-limit" || unresolved.length > 0; + return nativeCallObservationResultSchema.parse({ + target: { + path: target.path, + sha256: target.sha256, + architecture: target.architecture, + arguments: input.arguments, + working_directory: input.working_directory ?? null, + }, + debugger: trace.debugger, + process: { + pid: run.pid, + outcome: run.outcome, + exit_status: run.exit_status, + exit_description: run.exit_description, + terminated: run.terminated && trace.terminated, + elapsed_ms: run.elapsed_ms, + stdout: trace.stdout, + stderr: trace.stderr, + other_stops: run.other_stops, + }, + breakpoints: input.breakpoints.map((request, index) => ({ + index, + request: + request.kind === "function" + ? { ...request, module: request.module ?? null } + : { ...request, class_name: request.class_name ?? null }, + location_count: run.breakpoints[index]?.location_count ?? 0, + locations: run.breakpoints[index]?.locations ?? [], + })), + events: run.events, + coverage: { + status: partial ? "partial" : "complete", + event_limit_reached: eventLimit, + unresolved_breakpoints: unresolved, + }, + limitations: [ + ...NATIVE_CALL_OBSERVATION_LIMITATIONS, + ...(eventLimit + ? [ + `Observation stopped at max_events (${input.max_events}); later calls were not recorded and the process was killed.`, + ] + : []), + ...(run.outcome === "duration-elapsed" + ? [ + `The process was still running after duration_ms (${input.duration_ms}) and was killed.`, + ] + : []), + ...(run.outcome === "stop-limit" + ? [ + "Observation stopped after repeated signal or exception stops; the process was killed.", + ] + : []), + ...(unresolved.length === 0 + ? [] + : [ + `Breakpoints ${unresolved.join(", ")} matched no code in any image loaded while observing; their calls could not be observed.`, + ]), + ...(run.breakpoints.some( + ({ location_count: count, locations }) => locations.length < count, + ) + ? [ + "Breakpoints with many matching locations list only the first 64; location_count gives the total.", + ] + : []), + ...(trace.stdout.truncated || trace.stderr.truncated + ? ["Target output beyond 1 MiB per stream is counted but not kept."] + : []), + ...(run.terminated && trace.terminated + ? [] + : [ + `REA could not confirm that process ${run.pid} exited; check for it before relying on host state.`, + ]), + ], + }); +}; + +const fileSha256 = async ( + path: string, + signal?: AbortSignal, +): Promise => { + const handle = await open(path, "r"); + try { + const hash = createHash("sha256"); + const buffer = new Uint8Array(1024 * 1024); + for (;;) { + signal?.throwIfAborted(); + const { bytesRead } = await handle.read(buffer, 0, buffer.length, null); + if (bytesRead === 0) break; + hash.update(buffer.subarray(0, bytesRead)); + } + return hash.digest("hex"); + } finally { + await handle.close(); + } +}; diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index 45b5f272ba..666ccd7eba 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -5,6 +5,8 @@ import { export { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacOSProviderMetadata.js"; import { inspectAppleDispatchMetadata } from "./AppleDispatchMetadata.js"; import { observeNativeUi } from "./NativeUiObservation.js"; +import { LldbCallTracer, type NativeCallTracer } from "./LldbCallTracer.js"; +import { observeNativeCalls } from "./NativeCallObservation.js"; import { dirname, isAbsolute, resolve } from "node:path"; import { open, realpath, stat } from "node:fs/promises"; @@ -72,6 +74,7 @@ export class NativeMacOSProvider implements AnalysisProvider { constructor( private readonly runner: NativeCommandRunner = new XcrunCommandRunner(), platform: NodeJS.Platform = process.platform, + private readonly tracer: NativeCallTracer = new LldbCallTracer(), ) { this.#capabilities = nativeMacOSCapabilities(platform); } @@ -85,7 +88,7 @@ export class NativeMacOSProvider implements AnalysisProvider { } createClient(target: BinaryTarget): AnalysisClient { - return new NativeMacOSClient(target, this.runner); + return new NativeMacOSClient(target, this.runner, this.tracer); } } @@ -93,6 +96,7 @@ class NativeMacOSClient implements AnalysisClient { constructor( private readonly target: BinaryTarget, private readonly runner: NativeCommandRunner, + private readonly tracer: NativeCallTracer, ) {} async execute( @@ -157,6 +161,22 @@ class NativeMacOSClient implements AnalysisClient { "Operation is not implemented by native macOS tools.", ), ); + if (operation === "observe_native_calls") { + const result = await observeNativeCalls( + this.target, + parameters, + this.tracer, + options?.signal, + ); + return result.ok + ? ok( + createAnalysisExecution(result.value, IDENTITY, { + limitations: result.value.limitations, + locations: [{ kind: "artifact-path", path: this.target.path }], + }), + ) + : result; + } if ( operation === "observe_native_ui" || operation === "capture_native_ui_scenario" @@ -208,6 +228,7 @@ class NativeMacOSClient implements AnalysisClient { switch (operation) { case "observe_native_ui": case "capture_native_ui_scenario": + case "observe_native_calls": return Promise.resolve( err( new AnalysisCapabilityUnavailableError( diff --git a/src/native/NativeMacOSProviderMetadata.ts b/src/native/NativeMacOSProviderMetadata.ts index d5fc5ef137..3f7c24dbd0 100644 --- a/src/native/NativeMacOSProviderMetadata.ts +++ b/src/native/NativeMacOSProviderMetadata.ts @@ -12,6 +12,12 @@ export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ }); const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY; +/** Operations whose target process can show UI, use the network and write files. */ +const OWNED_PROCESS_OPERATIONS: ReadonlySet = new Set([ + "capture_native_ui_scenario", + "observe_native_calls", +]); + /** Declare native host coverage without creating a command runner. */ export const nativeMacOSCapabilities = ( platform: NodeJS.Platform = process.platform, @@ -36,10 +42,10 @@ export const nativeMacOSCapabilities = ( effects: Object.freeze({ mutatesArtifact: false, launchesProcess: contract.name !== "inspect_native_dispatch_metadata", - mayShowUi: contract.name === "capture_native_ui_scenario", - mayAccessNetwork: contract.name === "capture_native_ui_scenario", + mayShowUi: OWNED_PROCESS_OPERATIONS.has(contract.name), + mayAccessNetwork: OWNED_PROCESS_OPERATIONS.has(contract.name), mayWriteFilesystem: - contract.name === "capture_native_ui_scenario" || + OWNED_PROCESS_OPERATIONS.has(contract.name) || contract.name === "observe_native_ui", changesPermissions: false, requiresRoot: false, diff --git a/src/native/NativeUiHelperRuntime.ts b/src/native/NativeUiHelperRuntime.ts index e4571ec911..95e4888951 100644 --- a/src/native/NativeUiHelperRuntime.ts +++ b/src/native/NativeUiHelperRuntime.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url"; import { execFileOutput } from "../process/ExecFileOutput.js"; import { safeParseJson } from "../domain/safeJson.js"; import type { NativeUiHelper } from "./NativeUiObservation.js"; +import { NATIVE_UI_HELPER_MAX_BUFFER } from "./NativeUiOutputBudget.js"; /** Lazily compile one owned helper per observation/scenario and remove its compiler cache. */ export const createNativeUiHelperRuntime = () => { @@ -50,7 +51,7 @@ export const createNativeUiHelperRuntime = () => { [JSON.stringify(parameters)], { timeout: 30_000, - maxBuffer: 16 * 1024 * 1024, + maxBuffer: NATIVE_UI_HELPER_MAX_BUFFER, ...(signal === undefined ? {} : { signal }), }, ); diff --git a/src/native/NativeUiObservation.test.ts b/src/native/NativeUiObservation.test.ts index 0eda0258fb..b341ec6176 100644 --- a/src/native/NativeUiObservation.test.ts +++ b/src/native/NativeUiObservation.test.ts @@ -62,7 +62,6 @@ describe("native UI screenshot validation", () => { { base64: "%%%=" }, { base64: Buffer.from("not a PNG").toString("base64") }, { width: 2 }, - { sha256: "0".repeat(64) }, ])( "rejects invalid screenshot field variants from the helper", async (change) => { @@ -111,6 +110,37 @@ describe("native UI screenshot validation", () => { ); expect(result.ok).toBe(true); }); + + it("preserves an unknown AX child count and its truncation gap", async () => { + const partial = { + ...snapshot, + nodes: [ + { + path: [], + role: "AXWindow", + title: "Fixture", + value: null, + actions: [], + children_count: null, + }, + ], + truncated: true, + gaps: ["AX child count unavailable at path []: AXError -25204"], + }; + const result = await observeNativeUi( + target, + "observe_native_ui", + { ...scope, accessibility: true }, + { invoke: async () => ({ ok: true, result: partial }) }, + ); + expect(result.ok).toBe(true); + if (result.ok) + expect(result.value.initial).toMatchObject({ + truncated: true, + gaps: ["AX child count unavailable at path []: AXError -25204"], + nodes: [{ children_count: null }], + }); + }); }); describe("native UI capture selection and budgets", () => { @@ -205,6 +235,27 @@ describe("native UI capture selection and budgets", () => { outcome: "cancelled", }); }); + it("reports aggregate output exhaustion after individually valid captures", async () => { + const largeSnapshot = { + ...snapshot, + window: { ...snapshot.window, title: "x".repeat(17 * 1024 * 1024) }, + }; + const result = await observeNativeUi( + target, + "capture_native_ui_scenario", + { + ...scope, + steps: [{ kind: "wait", milliseconds: 0 }], + }, + { invoke: async () => ({ ok: true, result: largeSnapshot }) }, + ); + expect(result.ok).toBe(true); + if (result.ok) + expect(result.value.steps[0]).toMatchObject({ + outcome: "failed", + reason: expect.stringContaining("64 MiB output budget"), + }); + }); }); describe("native UI target and cancellation failures", () => { diff --git a/src/native/NativeUiObservation.ts b/src/native/NativeUiObservation.ts index a3273543a6..5d186875dc 100644 --- a/src/native/NativeUiObservation.ts +++ b/src/native/NativeUiObservation.ts @@ -16,6 +16,10 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; import { err, ok, type Result } from "../domain/result.js"; import { createNativeUiHelperRuntime } from "./NativeUiHelperRuntime.js"; +import { + NATIVE_UI_OUTPUT_BUDGET_BYTES, + NATIVE_UI_OUTPUT_WEIGHT, +} from "./NativeUiOutputBudget.js"; const helper = fileURLToPath( new URL("../../bridge/native/ReaNativeUI.swift", import.meta.url), @@ -135,8 +139,10 @@ const observeWithHelper = async ( ), ); launchTime = response.result.window.launch_time; - outputBytes += 2 * Buffer.byteLength(JSON.stringify(response.result)); - if (outputBytes > 64 * 1024 * 1024) + outputBytes += + NATIVE_UI_OUTPUT_WEIGHT * + Buffer.byteLength(JSON.stringify(response.result)); + if (outputBytes > NATIVE_UI_OUTPUT_BUDGET_BYTES) return err( new AnalysisCapabilityUnavailableError( "native-macos", diff --git a/src/native/NativeUiOutputBudget.ts b/src/native/NativeUiOutputBudget.ts new file mode 100644 index 0000000000..538c5cae37 --- /dev/null +++ b/src/native/NativeUiOutputBudget.ts @@ -0,0 +1,15 @@ +const HELPER_SUCCESS_WRAPPER = Buffer.from( + JSON.stringify({ ok: true, result: {} }), +); + +/** Scenario output is charged at twice the encoded result size before return. */ +export const NATIVE_UI_OUTPUT_BUDGET_BYTES = 64 * 1024 * 1024; +export const NATIVE_UI_OUTPUT_WEIGHT = 2; + +/** Bytes added around a result by the helper's success JSON response. */ +export const NATIVE_UI_HELPER_ENVELOPE_BYTES = + HELPER_SUCCESS_WRAPPER.byteLength - Buffer.byteLength("{}"); + +/** Raw helper output is bounded by the scenario's existing unweighted byte budget. */ +export const NATIVE_UI_HELPER_MAX_BUFFER = + NATIVE_UI_OUTPUT_BUDGET_BYTES + NATIVE_UI_HELPER_ENVELOPE_BYTES; diff --git a/src/native/parsers/demangle.test.ts b/src/native/parsers/demangle.test.ts deleted file mode 100644 index b18d40fd03..0000000000 --- a/src/native/parsers/demangle.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { expect, it } from "vitest"; -import { parseDemangledSymbols } from "./demangle.js"; - -it.each(["ordinary name ", "ordinary\t", " "])( - "preserves unchanged final symbol text: %j", - (input) => { - expect(parseDemangledSymbols([input], `${input}\n`)).toEqual([ - { input, output: input, status: "unchanged" }, - ]); - }, -); - -it.each(["carriage\r", "\r", "mid\rdle"])( - "keeps carriage returns that belong to the symbol: %j", - (input) => { - expect(parseDemangledSymbols([input], `${input}\n`)).toEqual([ - { input, output: input, status: "unchanged" }, - ]); - }, -); - -it("removes only the line terminator and preserves mixed output ordering", () => { - expect( - parseDemangledSymbols( - ["$s4main5helloyyF", "plain "], - "main.hello() -> ()\nplain \n", - ), - ).toEqual([ - { - input: "$s4main5helloyyF", - output: "main.hello() -> ()", - status: "demangled", - }, - { input: "plain ", output: "plain ", status: "unchanged" }, - ]); -}); diff --git a/src/native/pwndbg/PwndbgCoreContext.ts b/src/native/pwndbg/PwndbgCoreContext.ts new file mode 100644 index 0000000000..48f940549d --- /dev/null +++ b/src/native/pwndbg/PwndbgCoreContext.ts @@ -0,0 +1,232 @@ +import { access, mkdir, stat, writeFile } from "node:fs/promises"; +import { constants } from "node:fs"; +import { randomUUID } from "node:crypto"; +import { isAbsolute, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; +import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; +import type { AnalysisCapturedOutput } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; +import { + recordedCrashDebuggerSchema, + type RecordedCrash, +} from "../../domain/native/recordedCrash.js"; +import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; +import { runOwnedCommand } from "../../process/OwnedCommand.js"; +import { + capturedPwntoolsOutput, + pwntoolsDecoderFailure, +} from "../pwntools/PwntoolsFailures.js"; +import { PWNTOOLS_LIMITS } from "../pwntools/PwntoolsRelease.js"; +import { readPwntoolsFailureEvidence } from "../pwntools/PwntoolsResourceLimits.js"; +import type { PwntoolsLauncher } from "../pwntools/PwntoolsDecoder.js"; + +const OPERATION = "inspect_recorded_crash"; +const replySchema = z.discriminatedUnion("ok", [ + z.strictObject({ ok: z.literal(true), value: recordedCrashDebuggerSchema }), + z.strictObject({ + ok: z.literal(false), + reason: z.enum([ + "unavailable", + "unsupported", + "decoder", + "format", + "output-limit", + ]), + message: z.string(), + }), +]); + +/** Add mapping candidates from an owned core-only debugger, using the existing snapshot. */ +export const inspectPwndbgCore = async ({ + environment, + rootPath, + diagnostics, + options, + launcher, +}: { + readonly environment: Readonly; + readonly rootPath: string; + readonly diagnostics: AnalysisCapturedOutput; + readonly options: ExecutionOptions | undefined; + readonly launcher?: PwntoolsLauncher; +}): Promise => { + const gdb = environment.REA_PWNDBG_GDB ?? "/usr/bin/gdb"; + const entry = environment.REA_PWNDBG_GDBINIT ?? ""; + const venv = environment.REA_PWNDBG_VENV_PATH ?? ""; + const unavailable = (message: string, cause?: unknown) => + new AnalysisCapabilityUnavailableError("pwndbg-core", OPERATION, message, { + ...(cause === undefined ? {} : { cause }), + userMessage: message, + capturedOutput: diagnostics, + }); + for (const [key, path, kind] of [ + ["REA_PWNDBG_GDB", gdb, "file"], + ["REA_PWNDBG_GDBINIT", entry, "file"], + ["REA_PWNDBG_VENV_PATH", venv, "directory"], + ] as const) { + try { + if (!isAbsolute(path)) + throw new Error("Expected an absolute caller-supplied path."); + await access( + path, + key === "REA_PWNDBG_GDB" ? constants.X_OK : constants.R_OK, + ); + const information = await stat(path); + if (kind === "file" ? !information.isFile() : !information.isDirectory()) + throw new Error(`Expected a ${kind}.`); + } catch (cause: unknown) { + throw unavailable( + `${key} is unavailable at ${path}: ${cause instanceof Error ? cause.message : String(cause)}. Configure the requested core-only context, or omit include_debugger_context for basic recorded evidence.`, + cause, + ); + } + } + const stage = join(rootPath, "debugger"); + await mkdir(stage, { mode: 0o700 }); + const requestPath = join(stage, "request.json"); + const replyPath = join(stage, "reply.json"); + await writeFile( + requestPath, + JSON.stringify({ + snapshot_path: join(rootPath, "object.snapshot"), + reply_path: replyPath, + gdb, + gdbinit: entry, + venv, + }), + { flag: "wx", mode: 0o600 }, + ); + let output: AnalysisCapturedOutput | undefined; + try { + options?.signal?.throwIfAborted(); + const execution = await runOwnedCommand( + { + command: environment.REA_PWNTOOLS_PYTHON ?? "", + arguments: [ + "-I", + fileURLToPath( + new URL("../../../bridge/pwndbg/launch.py", import.meta.url), + ), + join(stage, "resource.failure"), + requestPath, + ], + cwd: stage, + runId: `rea-recorded-core-debugger-${randomUUID()}`, + hostEnvironment: { + ...environment, + PWNDBG_NO_AUTOUPDATE: "1", + PWNDBG_VENV_PATH: venv, + PWNLIB_NOTERM: "1", + TERM: "dumb", + HOME: stage, + XDG_CACHE_HOME: join(stage, "cache"), + XDG_CONFIG_HOME: join(stage, "config"), + PYTHONNOUSERSITE: "1", + OPENBLAS_NUM_THREADS: "1", + OMP_NUM_THREADS: "1", + }, + }, + { + timeoutMs: PWNTOOLS_LIMITS.timeoutMs, + diagnosticBytes: PWNTOOLS_LIMITS.diagnosticBytes, + }, + { + ...(options?.signal === undefined ? {} : { signal: options.signal }), + ...(launcher === undefined ? {} : { launcher }), + }, + ); + output = capturedPwntoolsOutput(execution); + let reply: z.output; + try { + const raw = await readStableArtifact( + replyPath, + PWNTOOLS_LIMITS.outputBytes, + options?.signal, + ); + reply = replySchema.parse(JSON.parse(raw.bytes.toString("utf8"))); + } catch (cause: unknown) { + if (options?.signal?.aborted) + throw new AnalysisCancelledError(OPERATION, { + capturedOutput: recordedCrashStageOutput(diagnostics, output), + }); + throw new AnalysisOutputError( + OPERATION, + `Core-only debugger reply failed: ${cause instanceof Error ? cause.message : String(cause)}`, + { + cause, + capturedOutput: recordedCrashStageOutput(diagnostics, output), + }, + ); + } + if (!reply.ok) { + if (reply.reason === "unavailable" || reply.reason === "unsupported") + throw new AnalysisCapabilityUnavailableError( + "pwndbg-core", + OPERATION, + reply.message, + { + userMessage: reply.message, + capturedOutput: recordedCrashStageOutput(diagnostics, output), + }, + ); + if (reply.reason === "format" || reply.reason === "output-limit") + throw new AnalysisOutputError(OPERATION, reply.message, { + capturedOutput: recordedCrashStageOutput(diagnostics, output), + }); + throw new ProviderAdapterError("pwndbg-core", OPERATION, { + diagnostics: { + stage: "debugger", + reason: reply.message, + captured_output: { ...recordedCrashStageOutput(diagnostics, output) }, + }, + }); + } + if (reply.value.status !== "available") + throw new AnalysisOutputError( + OPERATION, + "Requested debugger context returned no core evidence.", + { capturedOutput: recordedCrashStageOutput(diagnostics, output) }, + ); + return { ...reply.value, diagnostics: output }; + } catch (cause: unknown) { + if ( + options?.signal?.aborted && + (cause === options.signal.reason || + (cause instanceof Error && cause.name === "AbortError")) + ) + throw new AnalysisCancelledError(OPERATION, { + capturedOutput: recordedCrashStageOutput(diagnostics, output), + }); + throw pwntoolsDecoderFailure( + cause, + "debugger", + gdb, + environment.REA_PWNTOOLS_PYTHON ?? "", + await readPwntoolsFailureEvidence(cause, stage), + { + operation: OPERATION, + providerId: "pwndbg-core", + precedingOutput: diagnostics, + }, + ); + } +}; + +/** Preserve both stages' exact text and their independently observed truncation state. */ +export const recordedCrashStageOutput = ( + basic: AnalysisCapturedOutput, + debuggerOutput?: AnalysisCapturedOutput, +): AnalysisCapturedOutput => + debuggerOutput === undefined + ? basic + : { + stdout: `[core decoder]\n${basic.stdout}\n[debugger]\n${debuggerOutput.stdout}`, + stderr: `[core decoder]\n${basic.stderr}\n[debugger]\n${debuggerOutput.stderr}`, + truncated: basic.truncated || debuggerOutput.truncated, + }; diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts new file mode 100644 index 0000000000..eacc2d63e5 --- /dev/null +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -0,0 +1,333 @@ +import { + pwntoolsDecoderFailure, + pwntoolsUnavailable, + capturedPwntoolsOutput, +} from "./PwntoolsFailures.js"; +import { randomUUID } from "node:crypto"; +import { access, stat, writeFile } from "node:fs/promises"; +import { constants } from "node:fs"; +import { isAbsolute, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; +import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; +import type { ProviderIdentity } from "../../application/AnalysisProvider.js"; +import type { AnalysisCapturedOutput } from "../../domain/analysisErrorBase.js"; +import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisUnsupportedTargetError, + AnalysisInputError, + AnalysisOutputError, + AnalysisResourceConstraintError, +} from "../../domain/analysisErrorCore.js"; +import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; +import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { runOwnedCommand } from "../../process/OwnedCommand.js"; +import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; +import { + pwntoolsResourceLimitsSchema, + readPwntoolsFailureEvidence, +} from "./PwntoolsResourceLimits.js"; +import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; +import { + PWNTOOLS_PROVIDER_IDENTITY, + PWNTOOLS_LIMITS, +} from "./PwntoolsRelease.js"; + +/** Owned launcher seam shared by the offline pwntools artifact adapters. */ +export type PwntoolsLauncher = NonNullable< + Parameters[2] +>["launcher"]; +interface DecoderReport { + readonly diagnostics: AnalysisCapturedOutput; +} +/** Adapter-selected semantics around a shared owned pwntools process boundary. */ +export interface PwntoolsDecoderProfile< + Input extends { readonly path: string }, + Payload, + Report extends DecoderReport, +> { + readonly identity: ProviderIdentity; + readonly operation: string; + readonly bridge: URL; + readonly payloadSchema: z.ZodType; + readonly project: (context: { + readonly input: Input; + readonly payload: Payload; + readonly snapshot: { readonly bytes: Buffer; readonly sha256: string }; + readonly rootPath: string; + readonly diagnostics: AnalysisCapturedOutput; + readonly options: ExecutionOptions | undefined; + }) => Report | Promise; +} + +/** Thin owned-process adapter; all ELF parsing and heuristics come from unchanged upstream code. */ +export class PwntoolsDecoder< + Input extends { readonly path: string }, + Payload, + Report extends DecoderReport, +> { + readonly identity: ProviderIdentity; + constructor( + readonly profile: PwntoolsDecoderProfile, + readonly environment: Readonly, + readonly launcher?: PwntoolsLauncher, + readonly createRuntime: () => Promise< + Pick + > = () => PrivateRuntimeRoot.create({ prefix: "rea-artifact-decoder-" }), + ) { + this.identity = profile.identity; + } + + /** Snapshot one explicit artifact and return complete static observations without launching the selected object as a host process. */ + async inspect( + input: Input, + options?: ExecutionOptions, + ): Promise> { + const OPERATION = this.profile.operation; + const context = { operation: OPERATION, providerId: this.identity.id }; + const replySchema = z.discriminatedUnion("ok", [ + z.strictObject({ + ok: z.literal(true), + profile: z.literal(PWNTOOLS_PROVIDER_IDENTITY.version), + value: this.profile.payloadSchema, + }), + z.strictObject({ + ok: z.literal(false), + reason: z.enum([ + "format", + "unsupported", + "unsupported-target", + "unavailable", + "resource-limit", + "output-limit", + "decoder", + ]), + message: z.string(), + reported_limits: pwntoolsResourceLimitsSchema.nullable().optional(), + }), + ]); + let root: Pick | undefined; + let result: Result; + let phase: "configuration" | "artifact-read" | "decoder" = "configuration"; + const executablePath = this.environment.REA_PWNTOOLS_PYTHON ?? ""; + let selectedPath = executablePath; + try { + if (options?.signal?.aborted) throw new AnalysisCancelledError(OPERATION); + if (process.platform !== "linux" || process.arch !== "x64") + throw new AnalysisCapabilityUnavailableError( + this.identity.id, + OPERATION, + "The initial real-verified pwntools artifact profile supports Linux x64 only.", + ); + if (!isAbsolute(selectedPath)) + throw pwntoolsUnavailable( + "Set REA_PWNTOOLS_PYTHON to an absolute caller-supplied Python executable with pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2; REA never installs Python or packages.", + selectedPath, + undefined, + undefined, + context, + ); + await access(selectedPath, constants.X_OK); + if (!(await stat(selectedPath)).isFile()) + throw pwntoolsUnavailable( + "Selected Python executable must be a regular executable file, not a directory: " + + selectedPath, + selectedPath, + undefined, + undefined, + context, + ); + phase = "artifact-read"; + selectedPath = input.path; + const snapshot = await readStableArtifact( + input.path, + PWNTOOLS_LIMITS.inputBytes, + options?.signal, + ); + phase = "decoder"; + root = await this.createRuntime(); + const requestPath = join(root.path, "request.json"); + const snapshotPath = join(root.path, "object.snapshot"); + const replyPath = join(root.path, "reply.json"); + await writeFile(snapshotPath, snapshot.bytes, { + flag: "wx", + mode: 0o600, + ...(options?.signal === undefined ? {} : { signal: options.signal }), + }); + await writeFile( + requestPath, + JSON.stringify({ snapshot_path: snapshotPath, reply_path: replyPath }), + { flag: "wx", mode: 0o600 }, + ); + const execution = await runOwnedCommand( + { + command: this.environment.REA_PWNTOOLS_PYTHON ?? "", + arguments: [ + "-I", + fileURLToPath(this.profile.bridge), + join(root.path, "resource.failure"), + requestPath, + ], + cwd: root.path, + runId: `rea-artifact-decoder-${randomUUID()}`, + hostEnvironment: { + ...this.environment, + PWNLIB_NOTERM: "1", + OPENBLAS_NUM_THREADS: "1", + OMP_NUM_THREADS: "1", + }, + }, + { + timeoutMs: PWNTOOLS_LIMITS.timeoutMs, + diagnosticBytes: PWNTOOLS_LIMITS.diagnosticBytes, + }, + { + ...(options?.signal === undefined ? {} : { signal: options.signal }), + ...(this.launcher === undefined ? {} : { launcher: this.launcher }), + }, + ); + const capturedOutput = capturedPwntoolsOutput(execution); + let reply: z.output; + try { + const file = await readStableArtifact( + replyPath, + PWNTOOLS_LIMITS.outputBytes, + options?.signal, + ); + reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8"))); + } catch (cause: unknown) { + if (options?.signal?.aborted) + throw new AnalysisCancelledError(OPERATION, { capturedOutput }); + throw new AnalysisOutputError( + OPERATION, + `Owned pwntools decoder reply failed for ${input.path}: ${cause instanceof z.ZodError ? cause.issues[0]?.message + " at " + (cause.issues[0]?.path.map(String).join(".") ?? "root") : cause instanceof Error ? cause.message : String(cause)}`, + { cause, capturedOutput }, + ); + } + if (!reply.ok) { + if (reply.reason === "unsupported-target") + throw new AnalysisUnsupportedTargetError( + OPERATION, + input.path, + reply.message, + { capturedOutput }, + ); + if (reply.reason === "resource-limit") + throw new AnalysisResourceConstraintError( + OPERATION, + "memory", + reply.message, + reply.reported_limits ?? null, + { capturedOutput }, + ); + if (reply.reason === "format") + throw new AnalysisInputError(OPERATION, { capturedOutput }, [ + { + path: ["path"], + reason: "invalid_format", + message: reply.message, + }, + ]); + if (reply.reason === "unavailable") + throw pwntoolsUnavailable( + reply.message, + this.environment.REA_PWNTOOLS_PYTHON ?? "", + undefined, + capturedOutput, + context, + ); + if (reply.reason === "unsupported") + throw new AnalysisCapabilityUnavailableError( + this.identity.id, + OPERATION, + reply.message, + { userMessage: reply.message, capturedOutput }, + ); + if (reply.reason === "output-limit") + throw new AnalysisOutputError(OPERATION, reply.message, { + capturedOutput, + }); + throw new ProviderAdapterError(this.identity.id, OPERATION, { + diagnostics: { + phase, + failure_kind: reply.reason, + reason: reply.message, + stdout: execution.stdout.text, + stderr: execution.stderr.text, + captured_output: { ...capturedOutput }, + }, + }); + } + result = ok( + await this.profile.project({ + input, + payload: reply.value, + snapshot, + rootPath: root.path, + diagnostics: capturedOutput, + options, + }), + ); + } catch (cause: unknown) { + const evidence = await readPwntoolsFailureEvidence(cause, root?.path); + result = err( + options?.signal?.aborted && + (cause === options.signal.reason || + (cause instanceof Error && cause.name === "AbortError")) + ? new AnalysisCancelledError(OPERATION) + : pwntoolsDecoderFailure( + cause, + phase, + selectedPath, + executablePath, + evidence, + context, + ), + ); + } + if (root !== undefined) { + try { + await root.close(); + } catch (cause: unknown) { + return err( + new ProviderCleanupError( + this.identity.id, + [root.path], + { + reason: cause instanceof Error ? cause.message : String(cause), + previous_error: result.ok + ? null + : projectAnalysisError(result.error), + ...(result.ok + ? { + captured_output: { + stdout: result.value.diagnostics.stdout, + stderr: result.value.diagnostics.stderr, + truncated: result.value.diagnostics.truncated, + }, + } + : {}), + }, + { operation: OPERATION }, + ), + ); + } + } + return result.ok && options?.signal?.aborted + ? err( + new AnalysisCancelledError(OPERATION, { + capturedOutput: { + stdout: result.value.diagnostics.stdout, + stderr: result.value.diagnostics.stderr, + truncated: result.value.diagnostics.truncated, + }, + }), + ) + : result; + } +} diff --git a/src/native/pwntools/PwntoolsFailures.ts b/src/native/pwntools/PwntoolsFailures.ts new file mode 100644 index 0000000000..f5e1b26545 --- /dev/null +++ b/src/native/pwntools/PwntoolsFailures.ts @@ -0,0 +1,292 @@ +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; +import { + AnalysisError, + type AnalysisCapturedOutput, +} from "../../domain/analysisErrorBase.js"; +import type { ProviderProcessSnapshot } from "../../process/ProviderProcess.js"; +import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, + AnalysisTimeoutError, + AnalysisResourceConstraintError, +} from "../../domain/analysisErrorCore.js"; +import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; +import { ProviderSelectionError } from "../../domain/providerSelectionError.js"; +import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; +import type { PwntoolsFailureEvidence } from "./PwntoolsResourceLimits.js"; +import { + PWNTOOLS_PROVIDER_IDENTITY, + PWNTOOLS_LIMITS, + PWNTOOLS_MEMORY_FAILURE_EXIT, + PWNTOOLS_FILE_SIZE_FAILURE_EXIT, +} from "./PwntoolsRelease.js"; + +/** Operation identity supplied by each adapter sharing the pwntools boundary. */ +export interface PwntoolsFailureContext { + readonly operation: string; + readonly providerId: string; + readonly precedingOutput?: AnalysisCapturedOutput; +} +const DEFAULT_CONTEXT = { + operation: "inspect_binary_layout", + providerId: PWNTOOLS_PROVIDER_IDENTITY.id, +}; + +/** Keep read, engine, output and owned-lifecycle failure reasons distinct. */ +export const pwntoolsDecoderFailure = ( + cause: unknown, + phase: string, + path: string, + executablePath = path, + failureEvidence: PwntoolsFailureEvidence = {}, + context: PwntoolsFailureContext = DEFAULT_CONTEXT, +): AnalysisError => { + const limitReport = failureEvidence.limits; + if (cause instanceof AnalysisError) return cause; + if (cause instanceof OwnedCommandFailure) { + const currentOutput = + cause.snapshot === null + ? undefined + : capturedPwntoolsOutput(cause.snapshot); + const combinedOutput = + context.precedingOutput === undefined + ? currentOutput + : currentOutput === undefined + ? context.precedingOutput + : { + stdout: `[core decoder]\n${context.precedingOutput.stdout}\n[debugger]\n${currentOutput.stdout}`, + stderr: `[core decoder]\n${context.precedingOutput.stderr}\n[debugger]\n${currentOutput.stderr}`, + truncated: + context.precedingOutput.truncated || currentOutput.truncated, + }; + const outputOptions = + combinedOutput === undefined + ? undefined + : { capturedOutput: combinedOutput }; + if (cause.cleanupFailure !== null) + return new ProviderCleanupError( + context.providerId, + cause.resources, + { + reason: cause.cleanupFailure, + ...(combinedOutput === undefined + ? {} + : { + captured_output: { ...combinedOutput }, + }), + previous_error: { + failure_kind: cause.reason, + message: cause.message, + exit_code: cause.snapshot?.exitCode ?? null, + signal: cause.snapshot?.signal ?? null, + stdout: cause.snapshot?.stdout.text ?? null, + stderr: cause.snapshot?.stderr.text ?? null, + }, + }, + { operation: context.operation, cause }, + ); + if (cause.reason === "cancelled") + return new AnalysisCancelledError(context.operation, outputOptions); + if (cause.reason === "timeout") + return new AnalysisTimeoutError( + context.operation, + PWNTOOLS_LIMITS.timeoutMs, + outputOptions, + ); + if (cause.reason === "output-limit") + return new AnalysisOutputError( + context.operation, + cause.message, + outputOptions, + ); + if ( + cause.reason === "process" && + (cause.snapshot?.signal === "SIGXFSZ" || + (failureEvidence.marker?.resource === "file-size" && + cause.snapshot?.exitCode === PWNTOOLS_FILE_SIZE_FAILURE_EXIT && + cause.snapshot.signal === null)) + ) + return new AnalysisResourceConstraintError( + context.operation, + "file-size", + (cause.snapshot.signal === "SIGXFSZ" + ? "Owned Python terminated with SIGXFSZ; the exact signal cause is unknown." + : "The owned Python bridge reported EFBIG while writing a file; the exact write failure cause is unknown.") + + (limitReport?.failure === null || limitReport === undefined + ? "" + : ` Effective limit report unavailable: ${limitReport.failure}`), + limitReport?.limits ?? null, + outputOptions, + ); + if (cause.reason === "process" && cause.snapshot?.signal === "SIGXCPU") + return new AnalysisResourceConstraintError( + context.operation, + "cpu", + "Owned Python terminated with SIGXCPU; the exact signal cause is unknown." + + (limitReport?.failure === null || limitReport === undefined + ? "" + : ` Effective limit report unavailable: ${limitReport.failure}`), + limitReport?.limits ?? null, + outputOptions, + ); + if ( + cause.reason === "process" && + failureEvidence.marker?.resource === "memory" && + cause.snapshot?.exitCode === PWNTOOLS_MEMORY_FAILURE_EXIT && + cause.snapshot.signal === null + ) + return new AnalysisResourceConstraintError( + context.operation, + "memory", + "The owned Python bridge reported a memory allocation failure without a structured reply; the exact allocation cause is unknown." + + (limitReport?.failure === null || limitReport === undefined + ? "" + : ` Effective limit report unavailable: ${limitReport.failure}`), + limitReport?.limits ?? null, + outputOptions, + ); + } + if ( + cause instanceof Error && + "code" in cause && + "syscall" in cause && + typeof cause.syscall === "string" && + cause.syscall.startsWith("spawn ") + ) + return pwntoolsUnavailable( + `Selected Python could not launch (${String(cause.code)}): ${executablePath}. Check the configured executable, its interpreter and host execute access.`, + executablePath, + String(cause.code), + undefined, + context, + ); + if (cause instanceof ArtifactReaderFailure) { + if (cause.reason === "integrity") + return new AnalysisArtifactChangedError( + context.operation, + path, + cause.message, + { + cause, + }, + ); + if (cause.reason === "cancelled") + return new AnalysisCancelledError(context.operation); + if ( + cause.reason === "limit" || + cause.reason === "path" || + cause.reason === "format" + ) + return new AnalysisInputError(context.operation, { cause }, [ + { + path: ["path"], + reason: cause.reason === "limit" ? "out_of_range" : "invalid_format", + message: cause.message, + }, + ]); + } + if (cause instanceof Error && "code" in cause) { + if (phase === "configuration") + return pwntoolsUnavailable( + `Selected Python executable is unavailable (${String(cause.code)}): ${path}. Check the caller-selected path and execute access.`, + path, + String(cause.code), + undefined, + context, + ); + if ( + phase === "artifact-read" && + (cause.code === "EACCES" || cause.code === "EPERM") + ) + return new AnalysisAccessDeniedError( + context.operation, + path, + cause.code, + { + cause, + }, + ); + if ( + phase === "artifact-read" && + (cause.code === "ENOENT" || cause.code === "ENOTDIR") + ) + return new AnalysisInputError(context.operation, { cause }, [ + { + path: ["path"], + reason: "invalid_value", + message: `Selected object could not be read (${String(cause.code)}): ${path}.`, + }, + ]); + } + return new ProviderAdapterError(context.providerId, context.operation, { + cause, + diagnostics: { + phase, + path, + ...(context.precedingOutput === undefined + ? {} + : { preceding_output: { ...context.precedingOutput } }), + reason: cause instanceof Error ? cause.message : String(cause), + ...(failureEvidence.marker === undefined + ? {} + : { resource_failure_marker: { ...failureEvidence.marker } }), + ...(cause instanceof OwnedCommandFailure + ? { + failure_kind: cause.reason, + exit_code: cause.snapshot?.exitCode ?? null, + signal: cause.snapshot?.signal ?? null, + stdout: cause.snapshot?.stdout.text ?? null, + stderr: cause.snapshot?.stderr.text ?? null, + ...(cause.snapshot === null + ? {} + : { + captured_output: { + ...capturedPwntoolsOutput(cause.snapshot), + }, + }), + } + : {}), + }, + }); +}; + +/** Report caller-selected engine absence with its actual configuration and host constraint. */ +export const pwntoolsUnavailable = ( + reason: string, + path: string, + systemCode?: string, + capturedOutput?: AnalysisCapturedOutput, + context: PwntoolsFailureContext = DEFAULT_CONTEXT, +): ProviderSelectionError => + new ProviderSelectionError({ + ...(capturedOutput === undefined ? {} : { capturedOutput }), + operation: context.operation, + reason: "provider_unavailable", + requestedProviderId: context.providerId, + candidateIds: [context.providerId], + rejections: [ + { + providerId: context.providerId, + code: "provider_unavailable", + reason, + diagnostics: { + configuration_key: "REA_PWNTOOLS_PYTHON", + executable_path: path, + ...(systemCode === undefined ? {} : { system_code: systemCode }), + }, + }, + ], + }); + +/** Retain bounded text output and the shared supervisor's observed truncation flag. */ +export const capturedPwntoolsOutput = ( + snapshot: ProviderProcessSnapshot, +): AnalysisCapturedOutput => ({ + stdout: snapshot.stdout.text, + stderr: snapshot.stderr.text, + truncated: snapshot.diagnosticTruncated === true, +}); diff --git a/src/native/pwntools/PwntoolsLayoutProvider.ts b/src/native/pwntools/PwntoolsLayoutProvider.ts new file mode 100644 index 0000000000..f9346e397b --- /dev/null +++ b/src/native/pwntools/PwntoolsLayoutProvider.ts @@ -0,0 +1,74 @@ +import { + binaryLayoutPayloadSchema, + binaryLayoutSchema, + type BinaryLayout, + type InspectBinaryLayoutInput, +} from "../../domain/native/binaryLayout.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; +import { PwntoolsDecoder, type PwntoolsLauncher } from "./PwntoolsDecoder.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsRelease.js"; +import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; +import type { BinaryLayoutPort } from "../../application/binaryDiagnostics/BinaryLayoutPort.js"; +import type { z } from "zod"; + +/** ELF-specific projection around the shared owned pwntools decoder boundary. */ +export class PwntoolsLayoutProvider + extends PwntoolsDecoder< + InspectBinaryLayoutInput, + z.output, + BinaryLayout + > + implements BinaryLayoutPort +{ + constructor( + environment: Readonly, + launcher?: PwntoolsLauncher, + createRuntime: () => Promise< + Pick + > = () => PrivateRuntimeRoot.create({ prefix: "rea-elf-layout-" }), + ) { + super( + { + identity: PWNTOOLS_PROVIDER_IDENTITY, + operation: "inspect_binary_layout", + bridge: new URL("../../../bridge/pwntools/layout.py", import.meta.url), + payloadSchema: binaryLayoutPayloadSchema, + project: ({ input, payload, snapshot, diagnostics }) => { + const value = binaryLayoutSchema.safeParse({ + ...payload, + artifact: { + path: input.path, + sha256: snapshot.sha256, + bytes: snapshot.bytes.length, + }, + diagnostics, + }); + if (!value.success) + throw new AnalysisOutputError( + "inspect_binary_layout", + `ELF reply contains invalid source ranges or value meanings: ${value.error.issues[0]?.message ?? "schema mismatch"}`, + { capturedOutput: diagnostics }, + ); + for (const table of value.data.packed_relative_relocations) { + const start = Number(BigInt(table.location.offset)); + const length = Number(BigInt(table.location.bytes)); + if ( + !Buffer.from(table.encoded_bytes_base64, "base64").equals( + snapshot.bytes.subarray(start, start + length), + ) + ) + throw new AnalysisOutputError( + "inspect_binary_layout", + "Reported packed relocation bytes differ from the selected snapshot at their original file range.", + { capturedOutput: diagnostics }, + ); + } + return value.data; + }, + }, + environment, + launcher, + createRuntime, + ); + } +} diff --git a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts new file mode 100644 index 0000000000..e0d1ea8d2b --- /dev/null +++ b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts @@ -0,0 +1,144 @@ +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; +import { + recordedCrashPayloadSchema, + recordedCrashSchema, + type InspectRecordedCrashInput, + type RecordedCrash, +} from "../../domain/native/recordedCrash.js"; +import type { RecordedCrashPort } from "../../application/binaryDiagnostics/RecordedCrashPort.js"; +import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; +import { PwntoolsDecoder, type PwntoolsLauncher } from "./PwntoolsDecoder.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsRelease.js"; +import { validateRecordedCrashSources } from "./RecordedCrashSourceBindings.js"; +import { validateRecordedCrashStructure } from "./RecordedCrashStructureBindings.js"; +import { + inspectPwndbgCore, + recordedCrashStageOutput, +} from "../pwndbg/PwndbgCoreContext.js"; +import type { z } from "zod"; + +/** Preserve every recorded thread while optionally enriching the same owned snapshot. */ +export class PwntoolsRecordedCrashProvider + extends PwntoolsDecoder< + InspectRecordedCrashInput, + z.output, + RecordedCrash + > + implements RecordedCrashPort +{ + constructor( + environment: Readonly, + launcher?: PwntoolsLauncher, + createRuntime: () => Promise< + Pick + > = () => PrivateRuntimeRoot.create({ prefix: "rea-recorded-crash-" }), + ) { + super( + { + identity: PWNTOOLS_PROVIDER_IDENTITY, + operation: "inspect_recorded_crash", + bridge: new URL( + "../../../bridge/pwntools/recorded_crash.py", + import.meta.url, + ), + payloadSchema: recordedCrashPayloadSchema, + project: async ({ + input, + payload, + snapshot, + rootPath, + diagnostics, + options, + }) => { + const validate = (debuggerContext: RecordedCrash["debugger"]) => + recordedCrashSchema.safeParse({ + ...payload, + artifact: { + path: input.path, + sha256: snapshot.sha256, + bytes: snapshot.bytes.length, + }, + debugger: debuggerContext, + decoder_diagnostics: diagnostics, + diagnostics: + debuggerContext.status === "available" + ? recordedCrashStageOutput( + diagnostics, + debuggerContext.diagnostics, + ) + : diagnostics, + }); + const checked = validate({ status: "not-requested" }); + if (!checked.success) + throw new AnalysisOutputError( + "inspect_recorded_crash", + `Recorded core reply is invalid: ${checked.error.issues[0]?.message ?? "schema mismatch"}`, + { capturedOutput: diagnostics }, + ); + const value = checked.data; + validateRecordedCrashStructure(value, snapshot.bytes, diagnostics); + const checkBytes = ( + raw: string, + range: { readonly offset: string; readonly bytes: string }, + ) => { + const start = Number(BigInt(range.offset)); + const length = Number(BigInt(range.bytes)); + if ( + !Buffer.from(raw, "base64").equals( + snapshot.bytes.subarray(start, start + length), + ) + ) + throw new AnalysisOutputError( + "inspect_recorded_crash", + "Reported raw note bytes differ from the selected snapshot.", + { capturedOutput: diagnostics }, + ); + }; + for (const note of value.notes) { + checkBytes(note.owner_bytes_base64, note.owner_location); + checkBytes(note.descriptor_bytes_base64, note.descriptor_location); + } + for (const padding of value.note_padding) + checkBytes(padding.bytes_base64, padding.location); + validateRecordedCrashSources(value, snapshot.bytes, diagnostics); + for (const thread of value.threads) + for (const register of thread.registers) { + const start = Number(BigInt(register.location.offset)); + if ( + snapshot.bytes.readBigUInt64LE(start) !== BigInt(register.value) + ) + throw new AnalysisOutputError( + "inspect_recorded_crash", + "Reported register value differs from its recorded source bytes.", + { capturedOutput: diagnostics }, + ); + } + if (!input.include_debugger_context) return value; + const context = await inspectPwndbgCore({ + environment, + rootPath, + diagnostics, + options, + ...(launcher === undefined ? {} : { launcher }), + }); + const enriched = validate(context); + if (!enriched.success) + throw new AnalysisOutputError( + "inspect_recorded_crash", + `Recorded debugger context is invalid: ${enriched.error.issues[0]?.message ?? "schema mismatch"}`, + { + capturedOutput: + context.status === "available" + ? recordedCrashStageOutput(diagnostics, context.diagnostics) + : diagnostics, + }, + ); + return enriched.data; + }, + }, + environment, + launcher, + createRuntime, + ); + } +} diff --git a/src/native/pwntools/PwntoolsRelease.ts b/src/native/pwntools/PwntoolsRelease.ts new file mode 100644 index 0000000000..f041304f16 --- /dev/null +++ b/src/native/pwntools/PwntoolsRelease.ts @@ -0,0 +1,19 @@ +/** Verified unchanged upstream profile for the Linux ELF adapter. */ +export const PWNTOOLS_PROVIDER_IDENTITY = { + id: "pwntools-elf", + name: "REA pwntools ELF adapter", + version: "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2", +} as const; +/** Bridge-reserved status for MemoryError when no structured reply can be written. */ +export const PWNTOOLS_MEMORY_FAILURE_EXIT = 75; +/** Bridge-reserved status for an observed EFBIG failure without a complete reply. */ +export const PWNTOOLS_FILE_SIZE_FAILURE_EXIT = 76; +/** Complete evidence budgets; address-space is separate from resident memory. */ +export const PWNTOOLS_LIMITS = { + inputBytes: 32 * 1024 * 1024, + outputBytes: 64 * 1024 * 1024, + diagnosticBytes: 1024 * 1024, + timeoutMs: 30_000, + addressSpaceBytes: 3 * 1024 * 1024 * 1024, + cpuSeconds: 30, +} as const; diff --git a/src/native/pwntools/PwntoolsResourceLimits.ts b/src/native/pwntools/PwntoolsResourceLimits.ts new file mode 100644 index 0000000000..dcbe91d5ab --- /dev/null +++ b/src/native/pwntools/PwntoolsResourceLimits.ts @@ -0,0 +1,114 @@ +import { join } from "node:path"; +import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; +import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; +import { + PWNTOOLS_MEMORY_FAILURE_EXIT, + PWNTOOLS_FILE_SIZE_FAILURE_EXIT, +} from "./PwntoolsRelease.js"; +import { z } from "zod"; +import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js"; + +/** Actual limits reported by the owned bridge after lowering inherited soft limits. */ +export const pwntoolsResourceLimitsSchema = z.strictObject({ + address_space_bytes: z + .number() + .int() + .nonnegative() + .max(PWNTOOLS_LIMITS.addressSpaceBytes), + cpu_seconds: z.number().int().nonnegative().max(PWNTOOLS_LIMITS.cpuSeconds), + file_size_bytes: z + .number() + .int() + .nonnegative() + .max(PWNTOOLS_LIMITS.outputBytes), +}); + +/** A missing or invalid report leaves limits unknown and retains its read failure. */ +export interface PwntoolsLimitReport { + readonly limits: z.output | null; + readonly failure: string | null; +} + +/** Private failure-branch evidence; a bare launcher exit code is insufficient. */ +export interface PwntoolsFailureEvidence { + readonly limits?: PwntoolsLimitReport; + readonly marker?: { + readonly resource: "memory" | "file-size" | null; + readonly failure: string | null; + }; +} + +/** Read only bounded owned reports; unverified reserved statuses stay unclassified. */ +export const readPwntoolsFailureEvidence = async ( + cause: unknown, + rootPath?: string, +): Promise => { + let limitReport: PwntoolsLimitReport | undefined; + let marker: PwntoolsFailureEvidence["marker"]; + if ( + cause instanceof OwnedCommandFailure && + cause.snapshot?.signal === null && + (cause.snapshot.exitCode === PWNTOOLS_MEMORY_FAILURE_EXIT || + cause.snapshot.exitCode === PWNTOOLS_FILE_SIZE_FAILURE_EXIT) && + rootPath !== undefined + ) { + try { + const reported = await readStableArtifact( + join(rootPath, "resource.failure"), + 1, + ); + const expected = + cause.snapshot.exitCode === PWNTOOLS_MEMORY_FAILURE_EXIT ? "M" : "F"; + if (!reported.bytes.equals(Buffer.from(expected, "ascii"))) + throw new Error( + "Private bridge failure marker does not match the observed exit status.", + ); + marker = { + resource: expected === "M" ? "memory" : "file-size", + failure: null, + }; + } catch (markerFailure: unknown) { + marker = { + resource: null, + failure: + markerFailure instanceof Error + ? markerFailure.message + : String(markerFailure), + }; + } + } + if ( + cause instanceof OwnedCommandFailure && + (cause.snapshot?.signal === "SIGXCPU" || + cause.snapshot?.signal === "SIGXFSZ" || + ((cause.snapshot?.exitCode === PWNTOOLS_FILE_SIZE_FAILURE_EXIT || + cause.snapshot?.exitCode === PWNTOOLS_MEMORY_FAILURE_EXIT) && + cause.snapshot.signal === null)) && + rootPath !== undefined + ) { + try { + const reported = await readStableArtifact( + join(rootPath, "limits.json"), + 4096, + ); + limitReport = { + limits: pwntoolsResourceLimitsSchema.parse( + JSON.parse(reported.bytes.toString("utf8")), + ), + failure: null, + }; + } catch (reportFailure: unknown) { + limitReport = { + limits: null, + failure: + reportFailure instanceof Error + ? reportFailure.message + : String(reportFailure), + }; + } + } + return { + ...(limitReport === undefined ? {} : { limits: limitReport }), + ...(marker === undefined ? {} : { marker }), + }; +}; diff --git a/src/native/pwntools/RecordedCrashSourceBindings.ts b/src/native/pwntools/RecordedCrashSourceBindings.ts new file mode 100644 index 0000000000..4de2555d2a --- /dev/null +++ b/src/native/pwntools/RecordedCrashSourceBindings.ts @@ -0,0 +1,68 @@ +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; +import { + RECORDED_LINUX_AMD64_REGISTERS, + type RecordedCrash, +} from "../../domain/native/recordedCrash.js"; + +const registerOffsets = new Map( + RECORDED_LINUX_AMD64_REGISTERS.map((name, index) => [name, 112 + index * 8]), +); + +/** Bind interpreted values and register identities to Linux amd64 source bytes. */ +export function validateRecordedCrashSources( + report: RecordedCrash, + snapshot: Buffer, + diagnostics: RecordedCrash["decoder_diagnostics"], +): void { + const fail = () => { + throw new AnalysisOutputError( + "inspect_recorded_crash", + "Reported PID or signal value differs from its recorded Linux amd64 source bytes.", + { capturedOutput: diagnostics }, + ); + }; + const descriptor = (noteIndex: number, minimumBytes: number): Buffer => { + const note = report.notes[noteIndex]; + if (note === undefined) return fail(); + const start = Number(BigInt(note.descriptor_location.offset)); + const length = Number(BigInt(note.descriptor_location.bytes)); + if (length < minimumBytes) return fail(); + return snapshot.subarray(start, start + length); + }; + // Fixed offsets independently checked against unchanged pwntools 4.15.0 + // elf_prstatus_amd64 / elf_siginfo_64; this is the sole supported note ABI. + for (const thread of report.threads) { + const bytes = descriptor(thread.note_index, 336); + if ( + thread.historical_pid !== bytes.readInt32LE(32) || + thread.recorded_current_signal !== bytes.readInt16LE(12) + ) + fail(); + const note = report.notes[thread.note_index]; + if (note === undefined) return fail(); + for (const register of thread.registers) { + const offset = registerOffsets.get(register.name); + if ( + offset === undefined || + BigInt(register.location.offset) !== + BigInt(note.descriptor_location.offset) + BigInt(offset) + ) + throw new AnalysisOutputError( + "inspect_recorded_crash", + "Reported register source does not match its Linux amd64 register identity.", + { capturedOutput: diagnostics }, + ); + } + } + for (const signal of report.signals) { + const bytes = descriptor(signal.note_index, 32); + if ( + signal.number !== bytes.readInt32LE(0) || + signal.errno !== bytes.readInt32LE(4) || + signal.code !== bytes.readInt32LE(8) || + (signal.fault_address !== null && + BigInt(signal.fault_address) !== bytes.readBigUInt64LE(16)) + ) + fail(); + } +} diff --git a/src/native/pwntools/RecordedCrashStructureBindings.ts b/src/native/pwntools/RecordedCrashStructureBindings.ts new file mode 100644 index 0000000000..4f80ef60d9 --- /dev/null +++ b/src/native/pwntools/RecordedCrashStructureBindings.ts @@ -0,0 +1,133 @@ +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; +import type { RecordedCrash } from "../../domain/native/recordedCrash.js"; + +// Exact amd64/core enum projections in the pinned pyelftools 0.33 profile. +const segmentKinds = new Map([ + [0, "PT_NULL"], + [1, "PT_LOAD"], + [2, "PT_DYNAMIC"], + [3, "PT_INTERP"], + [4, "PT_NOTE"], + [5, "PT_SHLIB"], + [6, "PT_PHDR"], + [7, "PT_TLS"], + [0x60000000, "PT_LOOS"], + [0x6fffffff, "PT_HIOS"], + [0x6474e550, "PT_GNU_EH_FRAME"], + [0x6474e551, "PT_GNU_STACK"], + [0x6474e552, "PT_GNU_RELRO"], + [0x6474e553, "PT_GNU_PROPERTY"], +]); +const noteKinds = new Map([ + [1, "NT_PRSTATUS"], + [2, "NT_FPREGSET"], + [3, "NT_PRPSINFO"], + [4, "NT_TASKSTRUCT"], + [6, "NT_AUXV"], + [0x53494749, "NT_SIGINFO"], + [0x46494c45, "NT_FILE"], +]); +const matchesKind = ( + raw: number, + reported: string | number, + kinds: ReadonlyMap, +): boolean => { + const known = kinds.get(raw); + return typeof reported === "string" + ? reported === known + : known === undefined && reported === raw; +}; + +const segmentMatches = ( + segment: RecordedCrash["segments"][number], + raw: Buffer, +): boolean => { + const type = raw.readUInt32LE(0); + return ( + matchesKind(type, segment.type, segmentKinds) && + BigInt(segment.flags) === BigInt(raw.readUInt32LE(4)) && + segment.file_backing === + (type === 0 || raw.readBigUInt64LE(32) === 0n ? "none" : "file") && + ( + [ + [segment.offset, 8], + [segment.virtual_address, 16], + [segment.physical_address, 24], + [segment.file_size, 32], + [segment.memory_size, 40], + [segment.alignment, 48], + ] as const + ).every( + ([field, relative]) => BigInt(field) === raw.readBigUInt64LE(relative), + ) + ); +}; + +/** Bind coverage-bearing ELF and note headers to the same snapshot as their payloads. */ +export function validateRecordedCrashStructure( + report: RecordedCrash, + snapshot: Buffer, + diagnostics: RecordedCrash["decoder_diagnostics"], +): void { + const fail = () => { + throw new AnalysisOutputError( + "inspect_recorded_crash", + "Reported core structure differs from its original ELF/note headers.", + { capturedOutput: diagnostics }, + ); + }; + const bytesAt = (offset: bigint, bytes: number): Buffer => { + if (offset < 0n || offset > BigInt(snapshot.length - bytes)) return fail(); + return snapshot.subarray(Number(offset), Number(offset) + bytes); + }; + const header = bytesAt(0n, 64); + if ( + !header + .subarray(0, 6) + .equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1])) || + ![0, 3].includes(header[7] ?? -1) || + header[8] !== 0 || + header.readUInt16LE(16) !== 4 || + header.readUInt16LE(18) !== 62 + ) + fail(); + const table = header.readBigUInt64LE(32); + const entryBytes = header.readUInt16LE(54); + let count = header.readUInt16LE(56); + if (count === 0xffff) { + if (header.readUInt16LE(58) < 64) return fail(); + count = bytesAt(header.readBigUInt64LE(40), 64).readUInt32LE(44); + } + if (count !== report.segments.length || (count > 0 && entryBytes < 56)) + fail(); + for (const segment of report.segments) { + const offset = table + BigInt(segment.index) * BigInt(entryBytes); + if ( + BigInt(segment.header_location.offset) !== offset || + BigInt(segment.header_location.bytes) !== BigInt(entryBytes) + ) + fail(); + const raw = bytesAt(offset, entryBytes); + if (!segmentMatches(segment, raw)) fail(); + } + for (const note of report.notes) { + const raw = bytesAt(BigInt(note.location.offset), 12); + if ( + BigInt(note.owner_location.bytes) !== BigInt(raw.readUInt32LE(0)) || + BigInt(note.descriptor_location.bytes) !== BigInt(raw.readUInt32LE(4)) || + !matchesKind(raw.readUInt32LE(8), note.type, noteKinds) + ) + fail(); + const owner = bytesAt( + BigInt(note.owner_location.offset), + raw.readUInt32LE(0), + ); + const terminator = owner.indexOf(0); + const display = + owner.length === 0 + ? null + : owner.subarray(0, terminator).toString("utf8"); + if ((owner.length > 0 && terminator < 0) || note.owner_display !== display) + fail(); + } +} diff --git a/src/process/DarwinProcessRunTokenReader.ts b/src/process/DarwinProcessRunTokenReader.ts new file mode 100644 index 0000000000..1766880637 --- /dev/null +++ b/src/process/DarwinProcessRunTokenReader.ts @@ -0,0 +1,361 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; + +import { safeParseJson } from "../domain/safeJson.js"; +import { execFileOutput } from "./ExecFileOutput.js"; +import type { + ProcessIdentityObservation, + ProcessRunTokenObservation, + ProcessTableEntry, +} from "./ProcessOwnership.js"; + +const responseSchema = z.strictObject({ + results: z.array( + z.discriminatedUnion("state", [ + z.strictObject({ + pid: z.number().int().positive(), + state: z.literal("readable"), + run_id: z.string().optional(), + reason: z.null().optional(), + }), + z.strictObject({ + pid: z.number().int().positive(), + state: z.literal("unavailable"), + run_id: z.union([z.string(), z.null()]).optional(), + reason: z.string().min(1), + }), + ]), + ), +}); +const identityResponseSchema = z.strictObject({ + results: z.array( + z.discriminatedUnion("state", [ + z.strictObject({ + pid: z.number().int().positive(), + state: z.literal("readable"), + identity: z.string().min(1), + reason: z.null().optional(), + }), + z.strictObject({ + pid: z.number().int().positive(), + state: z.literal("unavailable"), + identity: z.null().optional(), + reason: z.string().min(1), + }), + ]), + ), +}); + +const sourceFiles = [ + fileURLToPath( + new URL( + "../../bridge/process/ProcessRunTokenReader.swift", + import.meta.url, + ), + ), + fileURLToPath( + new URL( + "../../bridge/process/ProcessRunTokenReaderMain.swift", + import.meta.url, + ), + ), +]; + +/** The native Darwin process-ownership reader could not be prepared. */ +export class DarwinProcessOwnershipInspectionError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "DarwinProcessOwnershipInspectionError"; + } +} + +interface ProcessReaderCompilation { + readonly controller: AbortController; + promise: Promise; + waiters: number; + aborting: boolean; + settled: boolean; +} + +const abortReason = (signal: AbortSignal): unknown => { + if (signal.reason !== undefined) return signal.reason; + const error = new Error("The operation was aborted"); + error.name = "AbortError"; + return error; +}; + +const isExpectedAbort = (cause: unknown, signal: AbortSignal): boolean => + signal.aborted && + (cause === signal.reason || + (cause instanceof Error && cause.name === "AbortError")); + +const waitForCompilation = async ( + promise: Promise, + signal: AbortSignal | undefined, +): Promise => { + if (signal === undefined) return promise; + if (signal.aborted) throw abortReason(signal); + return new Promise((resolve, reject) => { + let completed = false; + const finish = (callback: () => void) => { + if (completed) return; + completed = true; + signal.removeEventListener("abort", onAbort); + callback(); + }; + const onAbort = () => finish(() => reject(abortReason(signal))); + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => finish(() => resolve(value)), + (cause: unknown) => finish(() => reject(cause)), + ); + if (signal.aborted) onAbort(); + }); +}; + +/** Compile and retain the narrow Darwin sysctl reader for this REA process. */ +export const createDarwinProcessRunTokenReader = ( + options: { readonly xcrun?: string } = {}, +) => { + let root: string | undefined; + let executable: string | undefined; + let compilation: ProcessReaderCompilation | undefined; + let closePromise: Promise | undefined; + let closed = false; + let exitCleanupInstalled = false; + + const cleanupAtExit = () => { + if (root !== undefined) rmSync(root, { recursive: true, force: true }); + }; + const cleanupRoot = async (target = root): Promise => { + if (target === undefined) return; + await rm(target, { recursive: true, force: true }); + if (root !== target) return; + root = undefined; + executable = undefined; + if (exitCleanupInstalled) process.removeListener("exit", cleanupAtExit); + exitCleanupInstalled = false; + }; + const runCompilation = async (signal: AbortSignal): Promise => { + let operationRoot: string | undefined; + try { + operationRoot = await mkdtemp(join(tmpdir(), "rea-process-token-")); + root = operationRoot; + process.once("exit", cleanupAtExit); + exitCleanupInstalled = true; + if (signal.aborted || closed) throw abortReason(signal); + const output = join(operationRoot, "reader"); + try { + await execFileOutput( + options.xcrun ?? "/usr/bin/xcrun", + [ + "swiftc", + "-module-cache-path", + join(operationRoot, "modules"), + ...sourceFiles, + "-o", + output, + ], + { + timeout: 60_000, + maxBuffer: 1024 * 1024, + signal, + }, + ); + } catch (cause: unknown) { + if (isExpectedAbort(cause, signal)) throw cause; + const code = + cause instanceof Error && "code" in cause + ? String(cause.code) + : "unknown"; + throw new DarwinProcessOwnershipInspectionError( + `macOS process ownership inspection requires the Apple Swift compiler via xcrun (compiler result: ${code})`, + { cause }, + ); + } + if (signal.aborted || closed) throw abortReason(signal); + executable = output; + return output; + } catch (cause: unknown) { + const ownsRoot = operationRoot !== undefined && root === operationRoot; + if (operationRoot !== undefined) { + if (ownsRoot) await cleanupRoot(operationRoot); + else await rm(operationRoot, { recursive: true, force: true }); + } + throw cause; + } + }; + + const beginCompilation = (): ProcessReaderCompilation => { + const controller = new AbortController(); + const operation: ProcessReaderCompilation = { + controller, + promise: runCompilation(controller.signal).finally(() => { + operation.settled = true; + if (compilation === operation) compilation = undefined; + }), + waiters: 0, + aborting: false, + settled: false, + }; + compilation = operation; + return operation; + }; + + const compile = async (signal?: AbortSignal): Promise => { + if (closed) throw new Error("Darwin process token reader is closed"); + signal?.throwIfAborted(); + while (true) { + if (closed) throw new Error("Darwin process token reader is closed"); + signal?.throwIfAborted(); + if (executable !== undefined) return executable; + if (compilation === undefined && root !== undefined) { + await cleanupRoot(); + continue; + } + let operation = compilation; + if (operation?.aborting === true) { + try { + await waitForCompilation(operation.promise, signal); + } catch (cause: unknown) { + if (signal !== undefined && isExpectedAbort(cause, signal)) + throw cause; + } + continue; + } + operation ??= beginCompilation(); + operation.waiters += 1; + try { + return await waitForCompilation(operation.promise, signal); + } finally { + operation.waiters -= 1; + if ( + operation.waiters === 0 && + !operation.settled && + !operation.aborting + ) { + operation.aborting = true; + operation.controller.abort(); + await operation.promise.catch(() => undefined); + } + } + } + }; + + const read = async ( + processes: readonly ProcessTableEntry[], + ): Promise> => { + if (processes.length === 0) return new Map(); + const binary = await compile(); + const pids = processes.map(({ pid }) => String(pid)); + const { stdout } = await execFileOutput(binary, pids, { timeout: 15_000 }); + const decoded = safeParseJson(stdout); + if (!decoded.ok) + throw new Error( + `macOS process token helper returned invalid JSON: ${decoded.error}`, + { cause: decoded.cause }, + ); + const parsed = responseSchema.safeParse(decoded.value); + if (!parsed.success) + throw new Error( + "macOS process token helper returned an invalid response", + { + cause: parsed.error, + }, + ); + const requested = new Set(processes.map(({ pid }) => pid)); + const observations = new Map(); + for (const result of parsed.data.results) { + if (!requested.has(result.pid) || observations.has(result.pid)) + throw new Error( + "macOS process token helper returned an unexpected PID set", + ); + observations.set( + result.pid, + result.state === "readable" + ? { state: "readable", runId: result.run_id } + : { + state: "unavailable", + reason: + result.reason ?? "process token observation is unavailable", + }, + ); + } + if (observations.size !== requested.size) + throw new Error( + "macOS process token helper returned an incomplete PID set", + ); + return observations; + }; + + const identities = async ( + processes: readonly ProcessTableEntry[], + signal?: AbortSignal, + ): Promise> => { + if (processes.length === 0) return new Map(); + const binary = await compile(signal); + const pids = processes.map(({ pid }) => String(pid)); + const { stdout } = await execFileOutput(binary, ["--identities", ...pids], { + timeout: 15_000, + ...(signal === undefined ? {} : { signal }), + }); + const decoded = safeParseJson(stdout); + if (!decoded.ok) + throw new Error( + `macOS process identity helper returned invalid JSON: ${decoded.error}`, + { cause: decoded.cause }, + ); + const parsed = identityResponseSchema.safeParse(decoded.value); + if (!parsed.success) + throw new Error( + "macOS process identity helper returned an invalid response", + { + cause: parsed.error, + }, + ); + const requested = new Set(processes.map(({ pid }) => pid)); + const observations = new Map(); + for (const result of parsed.data.results) { + if (!requested.has(result.pid) || observations.has(result.pid)) + throw new Error( + "macOS process identity helper returned an unexpected PID set", + ); + observations.set( + result.pid, + result.state === "readable" + ? { state: "readable", identity: result.identity } + : { state: "unavailable", reason: result.reason }, + ); + } + if (observations.size !== requested.size) + throw new Error( + "macOS process identity helper returned an incomplete PID set", + ); + return observations; + }; + + const close = (): Promise => { + if (closePromise !== undefined) return closePromise; + closed = true; + const pendingClose = (async () => { + const operation = compilation; + if (operation !== undefined && !operation.settled) { + operation.aborting = true; + operation.controller.abort(); + await operation.promise.catch(() => undefined); + } + await cleanupRoot(); + })().catch((cause: unknown) => { + if (closePromise === pendingClose) closePromise = undefined; + throw cause; + }); + closePromise = pendingClose; + return closePromise; + }; + + return { read, identities, prepare: compile, close }; +}; diff --git a/src/process/ExecFileOutput.test.ts b/src/process/ExecFileOutput.test.ts index 1dd81155c5..bb963987bf 100644 --- a/src/process/ExecFileOutput.test.ts +++ b/src/process/ExecFileOutput.test.ts @@ -23,4 +23,13 @@ describe("execFileOutput", () => { ), ).rejects.toMatchObject({ code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" }); }); + + it("retains both captured streams on a failed child process", async () => { + await expect( + execFileOutput(process.execPath, [ + "-e", + 'process.stdout.write("out"); process.stderr.write("err"); process.exitCode = 7;', + ]), + ).rejects.toMatchObject({ code: 7, stdout: "out", stderr: "err" }); + }); }); diff --git a/src/process/ExecFileOutput.ts b/src/process/ExecFileOutput.ts index 53360848a8..232a93a7c1 100644 --- a/src/process/ExecFileOutput.ts +++ b/src/process/ExecFileOutput.ts @@ -25,6 +25,8 @@ export const execFileOutput = ( }, (error, stdout, stderr) => { if (error !== null) { + Reflect.set(error, "stdout", stdout); + Reflect.set(error, "stderr", stderr); reject(error); return; } diff --git a/src/process/OwnedCommand.ts b/src/process/OwnedCommand.ts new file mode 100644 index 0000000000..1fa054c709 --- /dev/null +++ b/src/process/OwnedCommand.ts @@ -0,0 +1,198 @@ +import type { + OwnedProviderProcessSpawnOptions, + ProviderProcessSnapshot, + SpawnedOwnedProviderProcess, +} from "./ProviderProcess.js"; +import { + ProviderProcessSupervisor, + spawnOwnedProviderProcess, +} from "./ProviderProcess.js"; +import { cleanupOwnedProcessGroup } from "./ProcessOwnership.js"; +import { + ProviderStartupDeadline, + waitForAbortableDelay, +} from "./ProviderDeadline.js"; + +/** A short-lived command failed its lifecycle or complete-output contract. */ +export class OwnedCommandFailure extends Error { + constructor( + readonly reason: "cancelled" | "timeout" | "output-limit" | "process", + message: string, + readonly snapshot: ProviderProcessSnapshot | null = null, + readonly cleanupFailure: string | null = null, + options?: ErrorOptions, + readonly resources: readonly string[] = [], + ) { + super(message, options); + } +} + +/** Execute a single owned process with independent cleanup and bounded diagnostic retention. */ +export const runOwnedCommand = async ( + spawn: OwnedProviderProcessSpawnOptions, + limits: { readonly timeoutMs: number; readonly diagnosticBytes: number }, + options: { + readonly signal?: AbortSignal; + readonly launcher?: ( + input: OwnedProviderProcessSpawnOptions, + ) => Promise; + } = {}, +): Promise => { + const callerSignal = options.signal ?? spawn.signal; + if (callerSignal?.aborted) + throw new OwnedCommandFailure( + "cancelled", + "Command cancelled before launch.", + ); + const deadline = new ProviderStartupDeadline(limits.timeoutMs, callerSignal); + try { + return await collectOwnedCommand( + spawn, + limits, + options.launcher ?? spawnOwnedProviderProcess, + deadline, + callerSignal, + ); + } finally { + deadline.dispose(); + } +}; + +const collectOwnedCommand = async ( + spawn: OwnedProviderProcessSpawnOptions, + limits: { readonly diagnosticBytes: number }, + launcher: ( + input: OwnedProviderProcessSpawnOptions, + ) => Promise, + deadline: ProviderStartupDeadline, + callerSignal?: AbortSignal, +): Promise => { + const signal = deadline.signal; + let launched: SpawnedOwnedProviderProcess; + try { + launched = await launcher({ + ...spawn, + signal, + }); + } catch (cause: unknown) { + const interruption = deadline.interruption; + if (interruption !== undefined) + throw new OwnedCommandFailure( + interruption, + interruption === "cancelled" + ? "Command cancelled during ownership preparation." + : "Command deadline elapsed during ownership preparation.", + null, + null, + { cause }, + ); + throw cause; + } + let exceeded = false; + let processFailure: string | undefined; + const supervisor = new ProviderProcessSupervisor( + { + ...launched, + ownsProcessLifetime: true, + cleanup: + launched.cleanup ?? + (() => cleanupOwnedProcessGroup(launched.ownership)), + }, + { + maxDiagnosticBytes: limits.diagnosticBytes, + onDiagnostic: (event) => { + if (event.type === "output" && event.truncated === true) + exceeded = true; + if (event.type === "error") processFailure = event.message; + }, + }, + ); + let failure: OwnedCommandFailure | undefined; + try { + while (!(await supervisor.waitForOutputClose(10))) { + const interruption = deadline.interruption; + if (interruption !== undefined) + throw new OwnedCommandFailure( + interruption, + interruption === "cancelled" + ? "Command cancelled." + : "Command deadline elapsed.", + ); + if (exceeded) + throw new OwnedCommandFailure( + "output-limit", + "Command diagnostic output exceeded its complete-output budget.", + ); + if (processFailure !== undefined) + throw new OwnedCommandFailure("process", processFailure); + await waitForAbortableDelay(25, signal); + } + const snapshot = supervisor.snapshot(); + const interruption = deadline.interruption; + if (interruption !== undefined) + throw new OwnedCommandFailure( + interruption, + interruption === "cancelled" + ? "Command cancelled." + : "Command deadline elapsed.", + snapshot, + ); + if (exceeded) + throw new OwnedCommandFailure( + "output-limit", + "Command diagnostic output exceeded its complete-output budget.", + snapshot, + ); + if ( + processFailure !== undefined || + snapshot.exitCode !== 0 || + snapshot.signal !== null + ) + throw new OwnedCommandFailure( + "process", + processFailure ?? + "Command failed; inspect its exit status and diagnostics.", + snapshot, + ); + } catch (cause: unknown) { + failure = + cause instanceof OwnedCommandFailure + ? cause + : new OwnedCommandFailure( + "process", + "Command collection failed.", + null, + null, + { cause }, + ); + } + const stopped = await supervisor.stop(); + const snapshot = supervisor.snapshot(); + supervisor.dispose(); + if (stopped.status === "incomplete") + throw new OwnedCommandFailure( + failure?.reason ?? "process", + failure?.message ?? "Command cleanup failed.", + snapshot, + stopped.reason, + failure === undefined ? undefined : { cause: failure }, + [ + spawn.runId, + ...(spawn.cwd === undefined ? [] : [spawn.cwd]), + `pid:${launched.ownership.leaderPid}`, + `process-group:${launched.ownership.processGroupId}`, + ], + ); + if (failure !== undefined) + throw new OwnedCommandFailure( + failure.reason, + failure.message, + snapshot, + failure.cleanupFailure, + { cause: failure }, + failure.resources, + ); + if (callerSignal?.aborted) + throw new OwnedCommandFailure("cancelled", "Command cancelled.", snapshot); + return snapshot; +}; diff --git a/src/process/ProcessOwnership.identity.test.ts b/src/process/ProcessOwnership.identity.test.ts new file mode 100644 index 0000000000..3d79b281d4 --- /dev/null +++ b/src/process/ProcessOwnership.identity.test.ts @@ -0,0 +1,491 @@ +import { describe, expect, it, vi } from "vitest"; +import { + cleanupOwnedProcessGroup, + verifyNoTokenOwnedProcesses, + type ProcessOwnershipHost, + type ProcessTableEntry, +} from "./ProcessOwnership.js"; +import { + createSystemProcessOwnershipHost, + parseLinuxProcessStartTime, +} from "./ProcessOwnershipObservation.js"; + +const itOnLinux = process.platform === "linux" ? it : it.skip; + +describe("Linux process start identities", () => { + itOnLinux("reads Linux process start identities from proc stat", async () => { + const host = createSystemProcessOwnershipHost("linux", {}); + expect(host.processIdentities).toBeDefined(); + expect(host.captureBaseline).toBeDefined(); + const observedProcess = { + pid: process.pid, + parentPid: process.ppid, + processGroupId: process.pid, + state: "S", + command: process.execPath, + } satisfies ProcessTableEntry; + const identities = await host.processIdentities?.([observedProcess]); + expect(identities?.get(observedProcess.pid)).toMatchObject({ + state: "readable", + identity: expect.stringMatching(/^linux-starttime:\d+$/u), + }); + const baseline = await host.captureBaseline?.(); + expect(baseline).toContainEqual({ + pid: observedProcess.pid, + identity: expect.stringMatching(/^linux-starttime:\d+$/u), + }); + }); + + it("parses stat start time after a command name containing parentheses", () => { + const fields = Array.from({ length: 20 }, (_, index) => + index === 0 ? "S" : index === 19 ? "987654" : "0", + ); + expect( + parseLinuxProcessStartTime( + `42 (worker ) old ) name) ${fields.join(" ")}`, + ), + ).toBe("987654"); + expect( + parseLinuxProcessStartTime("42 (worker) S too-short"), + ).toBeUndefined(); + }); +}); + +describe("capture-local process ownership", () => { + it("validates and cleans an exact-token owner without a capture baseline", async () => { + const process: ProcessTableEntry = { + pid: 904, + parentPid: 1, + processGroupId: 904, + state: "S", + command: "generic-darwin-owner", + }; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve([process]), + environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }), + runTokens: (entries) => + Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", runId: "run-token" } as const, + ]), + ), + ), + processIdentities: (entries) => + Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", identity: "owner-start" } as const, + ]), + ), + ), + signalGroup, + }; + + await expect( + cleanupOwnedProcessGroup( + { + runId: "run-token", + leaderPid: process.pid, + processGroupId: process.processGroupId, + sweepTokenOwnedProcesses: true, + }, + host, + ), + ).resolves.toEqual({ cleaned: true, signaled: true }); + expect(signalGroup).toHaveBeenCalledWith(process.processGroupId, "SIGKILL"); + }); + + it.skipIf(typeof process.getuid !== "function")( + "cleans verified owners and records unrelated opaque candidates as unverified", + async () => { + const owned: ProcessTableEntry = { + pid: 905, + parentPid: 1, + processGroupId: 905, + state: "S", + command: "owned-detached-child", + }; + const opaque: ProcessTableEntry = { + pid: 906, + parentPid: 1, + processGroupId: 906, + state: "S", + command: "/usr/bin/unrelated-process", + }; + const malformed: ProcessTableEntry = { + ...opaque, + pid: 907, + processGroupId: 907, + uid: process.getuid?.() === 0 ? 1 : 0, + command: "/usr/bin/malformed-procargs-process", + }; + const sysctlDenied: ProcessTableEntry = { + ...opaque, + pid: 908, + processGroupId: 908, + uid: process.getuid?.() === 0 ? 1 : 0, + command: "/usr/bin/sysctl-denied-process", + }; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + platform: "darwin", + listProcesses: () => + Promise.resolve([owned, opaque, malformed, sysctlDenied]), + environment: (pid) => + pid === owned.pid + ? Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }) + : Promise.reject(new Error("permission denied")), + runTokens: (entries) => + Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + pid === owned.pid + ? { state: "readable", runId: "run-token" as const } + : { + state: "unavailable", + reason: + pid === malformed.pid + ? ("malformed_procargs" as const) + : pid === sysctlDenied.pid + ? ("sysctl_failed_1" as const) + : ("platform_binary_environment_withheld" as const), + }, + ]), + ), + ), + signalGroup, + }; + + await expect( + cleanupOwnedProcessGroup( + { + runId: "run-token", + leaderPid: 100, + processGroupId: 100, + sweepTokenOwnedProcesses: true, + }, + host, + ), + ).resolves.toMatchObject({ + cleaned: true, + signaled: true, + unverified: [ + { pid: 906, diagnostic: "platform_binary_environment_withheld" }, + { pid: 907, diagnostic: "malformed_procargs" }, + { pid: 908, diagnostic: "sysctl_failed_1" }, + ], + }); + expect(signalGroup).toHaveBeenCalledWith(owned.processGroupId, "SIGKILL"); + expect(signalGroup).not.toHaveBeenCalledWith( + opaque.processGroupId, + "SIGKILL", + ); + expect(signalGroup).not.toHaveBeenCalledWith( + malformed.processGroupId, + "SIGKILL", + ); + expect(signalGroup).not.toHaveBeenCalledWith( + sysctlDenied.processGroupId, + "SIGKILL", + ); + }, + ); +}); + +describe("capture-local process identity baselines", () => { + it("checks reused and unknown PIDs while skipping unchanged identities", async () => { + const tokenReads: number[][] = []; + const processes: readonly ProcessTableEntry[] = [ + { + pid: 899, + parentPid: 1, + processGroupId: 899, + state: "S", + command: "stable", + }, + { + pid: 900, + parentPid: 1, + processGroupId: 900, + state: "S", + command: "reused", + }, + { + pid: 902, + parentPid: 1, + processGroupId: 902, + state: "S", + command: "unknown", + }, + ]; + const host: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve(processes), + environment: () => Promise.resolve({}), + processIdentities: (entries) => + Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + pid === 902 + ? { state: "unavailable", reason: "fixture" } + : { + state: "readable", + identity: pid === 899 ? "stable-start" : "reused-start", + }, + ]), + ), + ), + runTokens: (entries) => { + tokenReads.push(entries.map(({ pid }) => pid)); + return Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { + state: "readable", + runId: pid === 900 ? "run-token" : undefined, + }, + ]), + ), + ); + }, + signalGroup: vi.fn(), + }; + + await expect( + verifyNoTokenOwnedProcesses("run-token", host, [ + { pid: 899, identity: "stable-start" }, + { pid: 900, identity: "old-start" }, + { pid: 902, identity: null }, + ]), + ).resolves.toEqual({ + cleaned: false, + reason: "process identity could not be revalidated", + failures: [ + { + pid: 902, + reason: "process-identity-unavailable", + diagnostic: "process identity was unavailable during token scan", + }, + ], + }); + expect(tokenReads).toEqual([[900]]); + }); + + it("revalidates fallback environment owners before signaling after PID reuse", async () => { + let identityReads = 0; + const process: ProcessTableEntry = { + pid: 910, + parentPid: 1, + processGroupId: 910, + state: "S", + command: "fallback-environment-owner", + }; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve([process]), + environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }), + processIdentities: (entries) => { + identityReads += 1; + const identity = identityReads < 3 ? "owned-start" : "reused-start"; + return Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", identity } as const, + ]), + ), + ); + }, + signalGroup, + }; + + await expect( + cleanupOwnedProcessGroup( + { + runId: "run-token", + leaderPid: 100, + processGroupId: 100, + sweepTokenOwnedProcesses: true, + captureBaseline: [{ pid: process.pid, identity: "old-start" }], + }, + host, + ), + ).resolves.toMatchObject({ + cleaned: false, + reason: "process identity could not be revalidated", + }); + expect(signalGroup).not.toHaveBeenCalled(); + expect(identityReads).toBeGreaterThanOrEqual(3); + }); +}); + +describe("process identity during token validation", () => { + it("revalidates identity after the final environment token read", async () => { + let environmentReads = 0; + let reusedDuringFinalRead = false; + const process: ProcessTableEntry = { + pid: 912, + parentPid: 1, + processGroupId: 912, + state: "S", + command: "reused-during-final-token-read", + }; + const signalGroup = vi.fn(); + const host: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve([process]), + environment: () => { + environmentReads += 1; + if (environmentReads === 3) reusedDuringFinalRead = true; + return Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }); + }, + processIdentities: (entries) => { + const identity = reusedDuringFinalRead ? "reused-start" : "owned-start"; + return Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", identity } as const, + ]), + ), + ); + }, + signalGroup, + }; + + await expect( + cleanupOwnedProcessGroup( + { + runId: "run-token", + leaderPid: 100, + processGroupId: 100, + sweepTokenOwnedProcesses: true, + captureBaseline: [{ pid: process.pid, identity: "old-start" }], + }, + host, + ), + ).resolves.toMatchObject({ + cleaned: false, + reason: "process identity could not be revalidated", + }); + expect(signalGroup).not.toHaveBeenCalled(); + expect(reusedDuringFinalRead).toBe(true); + }); + + it("fails closed when a candidate PID changes identity during token reading", async () => { + let identityReads = 0; + const process: ProcessTableEntry = { + pid: 911, + parentPid: 1, + processGroupId: 911, + state: "S", + command: "reused-during-token-read", + }; + const host: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve([process]), + environment: () => Promise.resolve({}), + processIdentities: (entries) => { + identityReads += 1; + const identity = identityReads === 1 ? "new-start" : "reused-start"; + return Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", identity } as const, + ]), + ), + ); + }, + runTokens: (entries) => + Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", runId: undefined } as const, + ]), + ), + ), + signalGroup: vi.fn(), + }; + + await expect( + verifyNoTokenOwnedProcesses("run-token", host, [ + { pid: process.pid, identity: "old-start" }, + ]), + ).resolves.toEqual({ + cleaned: false, + reason: "process identity could not be revalidated", + failures: [ + { + pid: process.pid, + reason: "process-identity-unavailable", + diagnostic: + "process identity changed or became unavailable during token validation", + }, + ], + }); + }); +}); + +describe("process identity before signaling", () => { + it("refuses to signal when PID identity changes after token validation", async () => { + const signalGroup = vi.fn(); + let identityReads = 0; + const process: ProcessTableEntry = { + pid: 903, + parentPid: 1, + processGroupId: 903, + state: "S", + command: "reused-before-signal", + }; + const host: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve([process]), + environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }), + processIdentities: (entries) => { + identityReads += 1; + const identity = identityReads < 3 ? "owned-start" : "reused-start"; + return Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", identity } as const, + ]), + ), + ); + }, + runTokens: (entries) => + Promise.resolve( + new Map( + entries.map(({ pid }) => [ + pid, + { state: "readable", runId: "run-token" } as const, + ]), + ), + ), + signalGroup, + }; + + await expect( + cleanupOwnedProcessGroup( + { + runId: "run-token", + leaderPid: 100, + processGroupId: 100, + sweepTokenOwnedProcesses: true, + captureBaseline: [{ pid: process.pid, identity: "old-start" }], + }, + host, + ), + ).resolves.toMatchObject({ + cleaned: false, + reason: "process identity could not be revalidated", + }); + expect(signalGroup).not.toHaveBeenCalled(); + expect(identityReads).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/src/process/ProcessOwnership.part2.test.ts b/src/process/ProcessOwnership.part2.test.ts index 7e6e81b4cb..9fed67e43b 100644 --- a/src/process/ProcessOwnership.part2.test.ts +++ b/src/process/ProcessOwnership.part2.test.ts @@ -5,36 +5,7 @@ import { type ProcessOwnershipHost, } from "./ProcessOwnership.js"; import { observeOwnedProcessLineage } from "./ProcessOwnershipObservation.js"; -const ownership = { - runId: "run-token", - leaderPid: 100, - processGroupId: 100, -}; -const host = ( - environments: Readonly>>>, -): { - readonly adapter: ProcessOwnershipHost; - readonly signalGroup: ReturnType; -} => { - const signalGroup = vi.fn(); - return { - adapter: { - listProcesses: () => - Promise.resolve( - Object.keys(environments).map((pid) => ({ - pid: Number(pid), - parentPid: Number(pid) === 100 ? 1 : 100, - processGroupId: 100, - state: "S", - command: "fixture", - })), - ), - environment: (pid) => Promise.resolve(environments[pid] ?? {}), - signalGroup, - }, - signalGroup, - }; -}; +import { host, ownership } from "./ProcessOwnership.fixture.js"; describe("owned process-group cleanup discovery", () => { it("signals token-owned groups that were reparented outside the launcher tree", async () => { const signalGroup = vi.fn(); @@ -209,3 +180,72 @@ describe("owned process-group cleanup discovery", () => { }); }); }); + +describe("process environment permission diagnostics", () => { + it.each(["EACCES", "EPERM"])( + "preserves %s while refusing to signal an unverifiable live process", + async (code) => { + const signalGroup = vi.fn(); + const diagnostic = `${code}: permission denied, open '/proc/900/environ'`; + const adapter: ProcessOwnershipHost = { + listProcesses: () => + Promise.resolve([ + { + pid: 900, + parentPid: 1, + processGroupId: 900, + state: "S", + command: "unverifiable-process", + }, + ]), + environment: () => Promise.reject(new Error(diagnostic)), + signalGroup, + }; + await expect( + cleanupOwnedProcessGroup( + { ...ownership, sweepTokenOwnedProcesses: true }, + adapter, + ), + ).resolves.toMatchObject({ + cleaned: false, + reason: `process ownership token could not be read for 1 live process(es): environment_errno_${code}=1`, + failures: [{ pid: 900, reason: "environment-unreadable", diagnostic }], + }); + expect(signalGroup).not.toHaveBeenCalled(); + }, + ); + + it.skipIf(typeof process.getuid !== "function")( + "records a foreign-uid unreadable candidate as unverified without signaling", + async () => { + const foreignUid = process.getuid?.() === 0 ? 1 : 0; + const signalGroup = vi.fn(); + const adapter: ProcessOwnershipHost = { + listProcesses: () => + Promise.resolve([ + { + pid: 900, + parentPid: 1, + processGroupId: 900, + uid: foreignUid, + state: "S", + command: "unverifiable-process", + }, + ]), + environment: () => Promise.reject(new Error("EINVAL: sysctl failed")), + signalGroup, + }; + await expect( + cleanupOwnedProcessGroup( + { ...ownership, sweepTokenOwnedProcesses: true }, + adapter, + ), + ).resolves.toMatchObject({ + cleaned: true, + signaled: false, + unverified: [{ pid: 900, diagnostic: "EINVAL: sysctl failed" }], + }); + expect(signalGroup).not.toHaveBeenCalled(); + }, + ); +}); diff --git a/src/process/ProcessOwnership.test.ts b/src/process/ProcessOwnership.test.ts index d565f0494d..a2a39f89af 100644 --- a/src/process/ProcessOwnership.test.ts +++ b/src/process/ProcessOwnership.test.ts @@ -6,36 +6,7 @@ import { import { parseProcessEnvironment } from "./ProcessOwnershipObservation.js"; import { selectCapturedProcessGroupIds } from "./ProcessOwnershipProcessTree.js"; import { matchesOwnedProcessCommand } from "./ProcessCommandIdentity.js"; -const ownership = { - runId: "run-token", - leaderPid: 100, - processGroupId: 100, -}; -const host = ( - environments: Readonly>>>, -): { - readonly adapter: ProcessOwnershipHost; - readonly signalGroup: ReturnType; -} => { - const signalGroup = vi.fn(); - return { - adapter: { - listProcesses: () => - Promise.resolve( - Object.keys(environments).map((pid) => ({ - pid: Number(pid), - parentPid: Number(pid) === 100 ? 1 : 100, - processGroupId: 100, - state: "S", - command: "fixture", - })), - ), - environment: (pid) => Promise.resolve(environments[pid] ?? {}), - signalGroup, - }, - signalGroup, - }; -}; +import { host, ownership } from "./ProcessOwnership.fixture.js"; describe("owned process-group cleanup discovery", () => { it("excludes sampled groups whose leader was outside the captured tree", () => { expect( @@ -93,6 +64,312 @@ describe("owned process-group cleanup discovery", () => { }); expect(signalGroup).toHaveBeenCalledWith(100, "SIGKILL"); }); +}); + +describe("opaque process neighbors during cleanup", () => { + it("cleans verified run-owned groups and records a withheld unrelated neighbor", async () => { + const processes = [ + { pid: 100, parentPid: 1, processGroupId: 100, command: "capture" }, + { + pid: 200, + parentPid: 1, + processGroupId: 200, + command: "detached-child", + }, + { + pid: 900, + parentPid: 1, + processGroupId: 900, + command: "/usr/bin/opaque-neighbor", + }, + ].map((process) => ({ ...process, state: "S" })); + const signalGroup = vi.fn(); + const identities = new Map( + processes.map(({ pid }) => [ + pid, + { state: "readable" as const, identity: `start-${String(pid)}` }, + ]), + ); + const adapter: ProcessOwnershipHost = { + platform: "darwin", + listProcesses: () => Promise.resolve(processes), + environment: (pid) => + Promise.resolve({ + REA_PROCESS_RUN_ID: pid === 900 ? "unowned" : "run-token", + }), + processIdentities: () => Promise.resolve(identities), + runTokens: (members) => + Promise.resolve( + new Map( + members.map(({ pid }) => [ + pid, + pid === 900 + ? { + state: "unavailable" as const, + reason: "platform_binary_environment_withheld", + } + : { state: "readable" as const, runId: "run-token" }, + ]), + ), + ), + signalGroup, + }; + + const result = await cleanupOwnedProcessGroup( + { + ...ownership, + sweepTokenOwnedProcesses: true, + captureBaseline: [], + }, + adapter, + ); + + expect(result).toMatchObject({ + cleaned: true, + signaled: true, + unverified: [ + { pid: 900, diagnostic: "platform_binary_environment_withheld" }, + ], + }); + expect(signalGroup.mock.calls).toEqual([ + [100, "SIGKILL"], + [200, "SIGKILL"], + ]); + expect(signalGroup).not.toHaveBeenCalledWith(900, "SIGKILL"); + }); + + it("fails closed for an unrelated unreadable candidate the OS should expose", async () => { + const processes = [ + { pid: 100, parentPid: 1, processGroupId: 100, command: "capture" }, + { + pid: 900, + parentPid: 1, + processGroupId: 900, + command: "opaque-neighbor", + }, + ].map((process) => ({ ...process, state: "S" })); + const signalGroup = vi.fn(); + const identities = new Map( + processes.map(({ pid }) => [ + pid, + { state: "readable" as const, identity: `start-${String(pid)}` }, + ]), + ); + const adapter: ProcessOwnershipHost = { + platform: "darwin", + listProcesses: () => Promise.resolve(processes), + environment: (pid) => + Promise.resolve({ + REA_PROCESS_RUN_ID: pid === 900 ? "unowned" : "run-token", + }), + processIdentities: () => Promise.resolve(identities), + runTokens: (members) => + Promise.resolve( + new Map( + members.map(({ pid }) => [ + pid, + pid === 900 + ? { + state: "unavailable" as const, + reason: "environment_unavailable", + } + : { state: "readable" as const, runId: "run-token" }, + ]), + ), + ), + signalGroup, + }; + + const result = await cleanupOwnedProcessGroup( + { + ...ownership, + sweepTokenOwnedProcesses: true, + captureBaseline: [], + }, + adapter, + ); + + expect(result).toMatchObject({ + cleaned: false, + reason: expect.stringContaining("environment_unavailable=1"), + }); + expect(signalGroup.mock.calls).toEqual([[100, "SIGKILL"]]); + expect(signalGroup).not.toHaveBeenCalledWith(900, "SIGKILL"); + }); +}); + +describe("related sweep candidates during cleanup", () => { + it.each(["environment_unavailable", "platform_binary_environment_withheld"])( + "fails closed for a related candidate with %s", + async (diagnostic) => { + const processes = [ + { + pid: 100, + parentPid: 1, + processGroupId: 100, + state: "S", + command: "capture", + }, + { + pid: 101, + parentPid: 100, + processGroupId: 100, + state: "S", + uid: process.getuid?.() === 0 ? 1 : 0, + command: "ready-hang", + }, + ]; + const signalGroup = vi.fn(); + const identities = new Map( + processes.map(({ pid }) => [ + pid, + { state: "readable" as const, identity: `start-${String(pid)}` }, + ]), + ); + const adapter: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve(processes), + environment: (pid) => + pid === 100 + ? Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }) + : Promise.reject(new Error("EACCES: permission denied")), + processIdentities: () => Promise.resolve(identities), + runTokens: (members) => + Promise.resolve( + new Map( + members.map(({ pid }) => [ + pid, + pid === 100 + ? { state: "readable" as const, runId: "run-token" } + : { + state: "unavailable" as const, + reason: diagnostic, + }, + ]), + ), + ), + signalGroup, + }; + + const result = await cleanupOwnedProcessGroup( + { + ...ownership, + sweepTokenOwnedProcesses: true, + captureBaseline: [], + }, + adapter, + ); + + expect(result).toMatchObject({ + cleaned: false, + reason: expect.stringContaining( + "process ownership token could not be read", + ), + }); + expect(signalGroup).not.toHaveBeenCalled(); + }, + ); +}); + +describe("sampled detached process groups during cleanup", () => { + it("signals only token-authenticated sampled groups and reports an unowned sample", async () => { + const processes = [ + { + pid: 100, + parentPid: 1, + processGroupId: 100, + state: "S", + command: "capture", + }, + { + pid: 200, + parentPid: 1, + processGroupId: 200, + state: "S", + command: "sanitized-detached-child", + }, + ]; + const signalGroup = vi.fn(); + const identities = new Map([ + [100, { state: "readable" as const, identity: "start-100" }], + [200, { state: "readable" as const, identity: "start-200" }], + ]); + const adapter: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve(processes), + environment: (pid) => + Promise.resolve(pid === 100 ? { REA_PROCESS_RUN_ID: "run-token" } : {}), + processIdentities: () => Promise.resolve(identities), + runTokens: (members) => + Promise.resolve( + new Map( + members.map(({ pid }) => [ + pid, + { + state: "readable" as const, + runId: pid === 100 ? "run-token" : undefined, + }, + ]), + ), + ), + signalGroup, + }; + + const result = await cleanupOwnedProcessGroup( + { + ...ownership, + sweepTokenOwnedProcesses: true, + sampledProcessGroupIds: [200], + }, + adapter, + ); + + expect(result).toMatchObject({ + cleaned: false, + reason: "process tree contains an unowned or PID-reused process", + failures: [{ pid: 200, reason: "run-token-mismatch" }], + }); + expect(signalGroup).toHaveBeenCalledTimes(1); + expect(signalGroup).toHaveBeenCalledWith(100, "SIGKILL"); + expect(signalGroup).not.toHaveBeenCalledWith(200, "SIGKILL"); + }); + + it("reports a live sampled group after the launcher has exited without signaling it", async () => { + const process = { + pid: 200, + parentPid: 1, + processGroupId: 200, + state: "S", + command: "sanitized-detached-child", + }; + const signalGroup = vi.fn(); + const adapter: ProcessOwnershipHost = { + listProcesses: () => Promise.resolve([process]), + environment: () => Promise.resolve({}), + runTokens: () => + Promise.resolve( + new Map([[200, { state: "readable" as const, runId: undefined }]]), + ), + signalGroup, + }; + + const result = await cleanupOwnedProcessGroup( + { + ...ownership, + sweepTokenOwnedProcesses: true, + sampledProcessGroupIds: [200], + }, + adapter, + ); + + expect(result).toMatchObject({ + cleaned: false, + reason: "process tree contains an unowned or PID-reused process", + failures: [{ pid: 200, reason: "run-token-mismatch" }], + }); + expect(signalGroup).not.toHaveBeenCalled(); + }); +}); + +describe("rooted process-group cleanup", () => { it("validates and signals rooted descendant groups once, root first", async () => { const processes = [ { pid: 100, parentPid: 1, processGroupId: 100 }, diff --git a/src/process/ProcessOwnership.ts b/src/process/ProcessOwnership.ts index d74961401c..eff856e6f2 100644 --- a/src/process/ProcessOwnership.ts +++ b/src/process/ProcessOwnership.ts @@ -2,8 +2,8 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { - createSystemProcessOwnershipHost, errorMessage, + systemProcessOwnershipHost as systemHost, } from "./ProcessOwnershipObservation.js"; import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js"; @@ -21,17 +21,37 @@ export interface OwnedProcessGroup { readonly expectedParentPid?: number; /** Scan all live processes for this run token during cleanup. */ readonly sweepTokenOwnedProcesses?: boolean; + /** Prelaunch process identities for this capture; absent on generic callers. */ + readonly captureBaseline?: ProcessOwnershipBaseline; + /** + * Groups observed in capture samples. Observation adds cleanup candidates but + * does not authorize signaling; every live member still needs token proof. + */ + readonly sampledProcessGroupIds?: readonly number[]; } +/** Capture-local baseline; null means the PID identity could not be established. */ +export type ProcessOwnershipBaseline = readonly { + readonly pid: number; + readonly identity: string | null; +}[]; + /** One entry from an operating-system process-table snapshot. */ export interface ProcessTableEntry { readonly pid: number; readonly parentPid: number; readonly processGroupId: number; + /** Effective owner when the process table reports it. */ + readonly uid?: number; readonly state: string; readonly command: string; } +/** Native process start identity; unavailable identities must never be skipped. */ +export type ProcessIdentityObservation = + | { readonly state: "readable"; readonly identity: string } + | { readonly state: "unavailable"; readonly reason: string }; + /** Token-verified process lineage retained for one owned provider run. */ export interface OwnedProcessLineage { readonly runId: string; @@ -64,11 +84,32 @@ export type ProcessLineageObservation = /** Narrow operating-system seam used to inspect processes and signal groups. */ export interface ProcessOwnershipHost { readonly platform?: NodeJS.Platform; - listProcesses(): Promise; + /** Prepare required native identity inspection before launching a child. */ + prepare?(signal?: AbortSignal): Promise; + /** Release owned native inspection helpers and their temporary files. */ + close?(): Promise; + /** List current processes; startup inspection may be cancelled by the caller. */ + listProcesses(signal?: AbortSignal): Promise; environment(pid: number): Promise>>; + /** Read only run-token values in one host operation when the OS supports it. */ + runTokens?( + processes: readonly ProcessTableEntry[], + ): Promise>; + /** Read stable identities; omit the signal during cleanup so it can finish. */ + processIdentities?( + processes: readonly ProcessTableEntry[], + signal?: AbortSignal, + ): Promise>; + /** Snapshot identities before launch, honoring startup cancellation. */ + captureBaseline?(signal?: AbortSignal): Promise; signalGroup(processGroupId: number, signal: NodeJS.Signals): void; } +/** Whether one live process exposed its ownership token to the host. */ +export type ProcessRunTokenObservation = + | { readonly state: "readable"; readonly runId: string | undefined } + | { readonly state: "unavailable"; readonly reason: string }; + /** Narrow Windows P0 seam for bounded process-tree termination. */ export interface WindowsProcessTreeHost { terminateTree(rootPid: number): Promise<"terminated" | "missing">; @@ -77,21 +118,52 @@ export interface WindowsProcessTreeHost { /** Per-member reason that token-verified cleanup failed closed. */ interface ProcessOwnershipValidationFailure { readonly pid: number; - readonly reason: "environment-unreadable" | "run-token-mismatch"; + readonly reason: + | "environment-unreadable" + | "run-token-mismatch" + | "process-identity-unavailable"; readonly diagnostic?: string; } +/** + * One live process the ownership sweep could not attribute or exonerate. + * Recorded as evidence instead of failing the capture: the process is not + * related to the owned tree, so cleanup cannot act on it and the operating + * system withheld or removed its token (for example, macOS strips the + * environment of Apple platform binaries, and foreign-uid processes return + * EINVAL for KERN_PROCARGS2). + */ +export interface OwnershipSweepUnverifiedProcess { + readonly pid: number; + readonly diagnostic: string; +} + +/** Capture-owned process structures used to classify sweep candidates. */ +export interface OwnershipSweepRelation { + readonly leaderPid: number; + readonly processGroupId: number; + readonly sampledProcessGroupIds?: readonly number[]; +} + /** Cleanup outcome with per-member diagnostics when ownership is uncertain. */ export type ProcessCleanupResult = - | { readonly cleaned: true; readonly signaled: boolean } + | { + readonly cleaned: true; + readonly signaled: boolean; + readonly unverified?: readonly OwnershipSweepUnverifiedProcess[]; + } | { readonly cleaned: false; readonly reason: string; readonly failures?: readonly { readonly pid: number; - readonly reason: "environment-unreadable" | "run-token-mismatch"; + readonly reason: + | "environment-unreadable" + | "run-token-mismatch" + | "process-identity-unavailable"; readonly diagnostic?: string; }[]; + readonly unverified?: readonly OwnershipSweepUnverifiedProcess[]; }; /** Token-verified liveness result used by post-root-exit settlement. */ @@ -128,8 +200,6 @@ export const cleanupWindowsProcessTree = async ( } }; -const systemHost = createSystemProcessOwnershipHost(); - const systemWindowsProcessTreeHost: WindowsProcessTreeHost = { async terminateTree(rootPid) { try { @@ -181,6 +251,7 @@ export const cleanupOwnedProcessGroup = async ( processGroupId, ownership, host, + plan.tokenOwnedIdentities, ); if ("cleaned" in revalidation) return revalidation; if (revalidation.empty) continue; @@ -197,13 +268,21 @@ export const cleanupOwnedProcessGroup = async ( }; } } - return { cleaned: true, signaled }; + if (plan.unresolved.length > 0) + return cleanupValidationFailure(plan.unresolved, plan.unverified); + return { + cleaned: true, + signaled, + ...(plan.unverified.length === 0 ? {} : { unverified: plan.unverified }), + }; }; /** Verify that no live process still carries an owned capture run token. */ export const verifyNoTokenOwnedProcesses = async ( runId: string, host: ProcessOwnershipHost = systemHost, + captureBaseline?: ProcessOwnershipBaseline, + relation?: OwnershipSweepRelation, ): Promise => { const processTable = await readLiveProcessTable(host); if (!processTable.available) @@ -215,11 +294,22 @@ export const verifyNoTokenOwnedProcesses = async ( processTable.processes, runId, host, + captureBaseline, + relation, ); - if (scan.failures.length > 0) return cleanupValidationFailure(scan.failures); - return scan.owned.length === 0 - ? { cleaned: true, signaled: false } - : { cleaned: false, reason: "token-owned process remained after cleanup" }; + if (scan.failures.length > 0) + return cleanupValidationFailure(scan.failures, scan.unverified); + if (scan.owned.length > 0) + return { + cleaned: false, + reason: "token-owned process remained after cleanup", + ...(scan.unverified.length === 0 ? {} : { unverified: scan.unverified }), + }; + return { + cleaned: true, + signaled: false, + ...(scan.unverified.length === 0 ? {} : { unverified: scan.unverified }), + }; }; const readLiveProcessTable = async ( @@ -243,6 +333,9 @@ const readLiveProcessTable = async ( interface OwnedCleanupPlan { readonly signalOrder: readonly number[]; + readonly tokenOwnedIdentities: ReadonlyMap; + readonly unresolved: readonly ProcessOwnershipValidationFailure[]; + readonly unverified: readonly OwnershipSweepUnverifiedProcess[]; } const createOwnedCleanupPlan = async ( @@ -250,12 +343,28 @@ const createOwnedCleanupPlan = async ( processes: readonly ProcessTableEntry[], host: ProcessOwnershipHost, ): Promise => { + const sampledProcessGroupIds = new Set( + (ownership.sampledProcessGroupIds ?? []).filter( + (processGroupId) => + Number.isSafeInteger(processGroupId) && processGroupId > 0, + ), + ); const tokenOwned = ownership.sweepTokenOwnedProcesses === true - ? await scanTokenOwnedProcesses(processes, ownership.runId, host) - : { owned: [], failures: [] }; - if (tokenOwned.failures.length > 0) - return cleanupValidationFailure(tokenOwned.failures); + ? await scanTokenOwnedProcesses( + processes, + ownership.runId, + host, + ownership.captureBaseline, + { + leaderPid: ownership.leaderPid, + processGroupId: ownership.processGroupId, + ...(sampledProcessGroupIds.size === 0 + ? {} + : { sampledProcessGroupIds: [...sampledProcessGroupIds] }), + }, + ) + : { owned: [], failures: [], unverified: [], identities: new Map() }; const tokenOwnedGroupIds = new Set( tokenOwned.owned.map(({ processGroupId }) => processGroupId), ); @@ -266,9 +375,25 @@ const createOwnedCleanupPlan = async ( if ( launcher === undefined && rootMembers.length === 0 && - tokenOwned.owned.length === 0 + tokenOwned.owned.length === 0 && + !processes.some(({ processGroupId }) => + sampledProcessGroupIds.has(processGroupId), + ) ) - return { cleaned: true, signaled: false }; + return tokenOwned.failures.length > 0 + ? { + signalOrder: [], + tokenOwnedIdentities: tokenOwned.identities, + unresolved: tokenOwned.failures, + unverified: tokenOwned.unverified, + } + : { + cleaned: true, + signaled: false, + ...(tokenOwned.unverified.length === 0 + ? {} + : { unverified: tokenOwned.unverified }), + }; let descendants: readonly ProcessTableEntry[] = []; if (launcher !== undefined) { const identityFailure = launcherIdentityFailure(launcher, ownership); @@ -282,6 +407,11 @@ const createOwnedCleanupPlan = async ( processGroupIds.add(descendant.processGroupId); for (const process of tokenOwned.owned) processGroupIds.add(process.processGroupId); + for (const processGroupId of sampledProcessGroupIds) + processGroupIds.add(processGroupId); + const signalableGroups = new Set(); + const unresolved = [...tokenOwned.failures]; + const unverifiableSampledGroups = new Set(); for (const processGroupId of processGroupIds) { if (processGroupId === ownership.processGroupId) continue; const groupLeader = processes.find( @@ -290,29 +420,70 @@ const createOwnedCleanupPlan = async ( ); if ( (groupLeader === undefined || !descendantPids.has(groupLeader.pid)) && - !tokenOwnedGroupIds.has(processGroupId) + !tokenOwnedGroupIds.has(processGroupId) && + !sampledProcessGroupIds.has(processGroupId) ) return { cleaned: false, reason: "descendant process-group leader identity could not be verified", }; + if ( + (groupLeader === undefined || !descendantPids.has(groupLeader.pid)) && + !tokenOwnedGroupIds.has(processGroupId) + ) + unverifiableSampledGroups.add(processGroupId); } const liveMembers = processes.filter(({ processGroupId }) => processGroupIds.has(processGroupId), ); - const failures = await processOwnershipFailures( - liveMembers, - ownership.runId, - host, - ); - if (failures.length > 0) return cleanupValidationFailure(failures); + for (const processGroupId of processGroupIds) { + const members = liveMembers.filter( + ({ processGroupId: observedGroupId }) => + observedGroupId === processGroupId, + ); + if (members.length === 0) continue; + const failures = await processOwnershipFailures( + members, + ownership.runId, + host, + tokenOwned.identities, + ); + if (failures.length === 0 && unverifiableSampledGroups.has(processGroupId)) + unresolved.push( + ...members.map(({ pid }) => ({ + pid, + reason: "process-identity-unavailable" as const, + diagnostic: + "sampled process-group leader identity could not be verified", + })), + ); + const scanFailures = tokenOwned.failures.filter(({ pid }) => + members.some((member) => member.pid === pid), + ); + if (failures.length > 0) unresolved.push(...failures); + if ( + failures.length === 0 && + scanFailures.length === 0 && + !unverifiableSampledGroups.has(processGroupId) + ) + signalableGroups.add(processGroupId); + } + if (ownership.sweepTokenOwnedProcesses !== true && unresolved.length > 0) + return cleanupValidationFailure(unresolved, tokenOwned.unverified); return { - signalOrder: [ownership.processGroupId].concat( - [...processGroupIds] - .filter((processGroupId) => processGroupId !== ownership.processGroupId) - .sort((left, right) => left - right), - ), + signalOrder: [...signalableGroups] + .filter((groupId) => processGroupIds.has(groupId)) + .sort((left, right) => + left === ownership.processGroupId + ? -1 + : right === ownership.processGroupId + ? 1 + : left - right, + ), + tokenOwnedIdentities: tokenOwned.identities, + unresolved, + unverified: tokenOwned.unverified, }; }; @@ -320,6 +491,7 @@ const revalidateOwnedProcessGroup = async ( processGroupId: number, ownership: OwnedProcessGroup, host: ProcessOwnershipHost, + expectedIdentities: ReadonlyMap, ): Promise<{ readonly empty: boolean } | ProcessCleanupResult> => { let members: readonly ProcessTableEntry[]; try { @@ -337,6 +509,7 @@ const revalidateOwnedProcessGroup = async ( members, ownership.runId, host, + expectedIdentities, ); if (failures.length > 0) return cleanupValidationFailure(failures); if (processGroupId === ownership.processGroupId) { @@ -363,20 +536,109 @@ const revalidateOwnedProcessGroup = async ( const cleanupValidationFailure = ( failures: readonly ProcessOwnershipValidationFailure[], -): ProcessCleanupResult => ({ - cleaned: false, - reason: failures.some(({ reason }) => reason === "run-token-mismatch") - ? "process tree contains an unowned or PID-reused process" - : "process ownership could not be revalidated", - failures, -}); + unverified: readonly OwnershipSweepUnverifiedProcess[] = [], +): ProcessCleanupResult => { + const unverifiedEntry = unverified.length === 0 ? {} : { unverified }; + const unreadable = failures.filter( + ({ reason }) => reason === "environment-unreadable", + ); + if (failures.some(({ reason }) => reason === "run-token-mismatch")) + return { + cleaned: false, + reason: "process tree contains an unowned or PID-reused process", + failures, + ...unverifiedEntry, + }; + if (failures.some(({ reason }) => reason === "process-identity-unavailable")) + return { + cleaned: false, + reason: "process identity could not be revalidated", + failures, + ...unverifiedEntry, + }; + if (unreadable.length > 0) { + const counts = new Map(); + for (const { diagnostic } of unreadable) { + const category = sanitizedTokenReadFailure(diagnostic); + counts.set(category, (counts.get(category) ?? 0) + 1); + } + const breakdown = [...counts] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([category, count]) => `${category}=${String(count)}`) + .join(", "); + return { + cleaned: false, + reason: `process ownership token could not be read for ${String(unreadable.length)} live process(es): ${breakdown}`, + failures, + ...unverifiedEntry, + }; + } + return { + cleaned: false, + reason: "process ownership could not be revalidated", + failures, + ...unverifiedEntry, + }; +}; + +const sanitizedTokenReadFailure = (diagnostic: string | undefined): string => { + if (diagnostic === "environment_unavailable") return diagnostic; + if (diagnostic === "platform_binary_environment_withheld") return diagnostic; + const environmentErrno = /^(EACCES|EPERM|ENOENT|ESRCH):/u.exec( + diagnostic ?? "", + ); + if (environmentErrno?.[1] !== undefined) + return `environment_errno_${environmentErrno[1]}`; + if (diagnostic === "apple_vector_unavailable") return diagnostic; + if (diagnostic === "ambiguous_environment_boundary") return diagnostic; + if (diagnostic === "malformed_procargs") return diagnostic; + if (diagnostic === "process_identity_changed_during_token_read") + return diagnostic; + if (diagnostic === "process_not_in_snapshot") return diagnostic; + const processTableFailure = /^process_table_failed_(\d+)$/u.exec( + diagnostic ?? "", + ); + if (processTableFailure?.[1] !== undefined) + return `process_table_errno_${processTableFailure[1]}`; + const sysctlFailure = /^sysctl_failed_(\d+)$/u.exec(diagnostic ?? ""); + if (sysctlFailure?.[1] !== undefined) + return `sysctl_errno_${sysctlFailure[1]}`; + if (diagnostic === "reader_failure") return diagnostic; + if (diagnostic === "duplicate_run_token") return diagnostic; + if (diagnostic === "invalid_run_token_encoding") return diagnostic; + return "other_unavailable"; +}; const processOwnershipFailures = async ( members: readonly ProcessTableEntry[], runId: string, host: ProcessOwnershipHost, + expectedIdentities: ReadonlyMap = new Map(), ): Promise => { const failures: ProcessOwnershipValidationFailure[] = []; + if (expectedIdentities.size > 0 && host.processIdentities !== undefined) { + const expectedMembers = members.filter(({ pid }) => + expectedIdentities.has(pid), + ); + const observed = await host.processIdentities(expectedMembers); + for (const member of expectedMembers) { + const expected = expectedIdentities.get(member.pid); + const current = observed.get(member.pid); + if ( + expected === null || + current?.state !== "readable" || + current.identity !== expected + ) { + if (await processIsGone(host, member.pid)) continue; + failures.push({ + pid: member.pid, + reason: "process-identity-unavailable", + diagnostic: + "process identity changed or became unavailable before signal", + }); + } + } + } for (const member of members) { try { if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !== runId) @@ -400,43 +662,335 @@ const processOwnershipFailures = async ( }); } } + if ( + failures.length === 0 && + expectedIdentities.size > 0 && + host.processIdentities !== undefined + ) { + const expectedMembers = members.filter(({ pid }) => + expectedIdentities.has(pid), + ); + const observed = await host.processIdentities(expectedMembers); + for (const member of expectedMembers) { + const expected = expectedIdentities.get(member.pid); + const current = observed.get(member.pid); + if ( + expected !== null && + current?.state === "readable" && + current.identity === expected + ) + continue; + if (await processIsGone(host, member.pid)) continue; + failures.push({ + pid: member.pid, + reason: "process-identity-unavailable", + diagnostic: + "process identity changed or became unavailable during token validation", + }); + } + } return failures; }; interface TokenOwnedProcessScan { readonly owned: readonly ProcessTableEntry[]; readonly failures: readonly ProcessOwnershipValidationFailure[]; + readonly unverified: readonly OwnershipSweepUnverifiedProcess[]; + readonly identities: ReadonlyMap; } +/** One sweep candidate whose run token could not be read. */ +interface UnreadableTokenCandidate { + readonly process: ProcessTableEntry; + readonly diagnostic: string; +} + +const TOKEN_READ_RETRY_ATTEMPTS = 3; +const TOKEN_READ_RETRY_DELAY_MS = 40; + +/** Darwin reports this when the kernel withholds a platform binary's environment. */ +const PLATFORM_BINARY_ENVIRONMENT_WITHHELD = + "platform_binary_environment_withheld"; + +const currentProcessUid = (): number | undefined => + typeof process.getuid === "function" ? process.getuid() : undefined; + +/** + * Whether the operating system provably withheld this candidate's run token: + * current macOS omits the environment of Apple platform binaries from + * KERN_PROCARGS2 and fails with EINVAL for processes owned by another + * account, and neither class can be token-verified by any caller. Such a + * candidate is recorded as unverified instead of failing an otherwise clean + * capture; every other unreadable candidate stays fail-closed. + */ +const environmentWithheldFromInspection = ( + entry: ProcessTableEntry, + diagnostic: string, +): boolean => { + const uid = currentProcessUid(); + if (uid !== undefined && entry.uid !== undefined && entry.uid !== uid) + return true; + return diagnostic === PLATFORM_BINARY_ENVIRONMENT_WITHHELD; +}; + const scanTokenOwnedProcesses = async ( processes: readonly ProcessTableEntry[], runId: string, host: ProcessOwnershipHost, + captureBaseline?: ProcessOwnershipBaseline, + relation?: OwnershipSweepRelation, ): Promise => { - const owned: ProcessTableEntry[] = []; - const failures: ProcessOwnershipValidationFailure[] = []; - for (const process of processes) { - try { - if ((await host.environment(process.pid)).REA_PROCESS_RUN_ID === runId) - owned.push(process); - } catch (cause: unknown) { - try { - const live = liveProcesses(await host.listProcesses()); - if (!live.some(({ pid }) => pid === process.pid)) continue; - } catch (recheckCause: unknown) { - failures.push({ - pid: process.pid, - reason: "environment-unreadable", - diagnostic: `${errorMessage(cause)}; process liveness recheck failed: ${errorMessage(recheckCause)}`, - }); - continue; - } - failures.push({ - pid: process.pid, - reason: "environment-unreadable", - diagnostic: errorMessage(cause), + let candidates = processes; + let candidateIdentities: ReadonlyMap = + new Map(); + if (host.processIdentities !== undefined) { + const current = await host.processIdentities(processes); + candidateIdentities = current; + if (captureBaseline !== undefined) { + const baseline = new Map( + captureBaseline.map(({ pid, identity }) => [pid, identity]), + ); + candidates = processes.filter((process) => { + const before = baseline.get(process.pid); + const now = current.get(process.pid); + return !( + before !== undefined && + before !== null && + now?.state === "readable" && + now.identity === before + ); }); } } - return { owned, failures }; + const failures: ProcessOwnershipValidationFailure[] = []; + if (captureBaseline !== undefined && host.processIdentities !== undefined) { + const stableCandidates: ProcessTableEntry[] = []; + for (const process of candidates) { + if (candidateIdentities.get(process.pid)?.state === "readable") { + stableCandidates.push(process); + continue; + } + if (!(await processIsGone(host, process.pid))) + failures.push({ + pid: process.pid, + reason: "process-identity-unavailable", + diagnostic: "process identity was unavailable during token scan", + }); + } + candidates = stableCandidates; + } + const owned: ProcessTableEntry[] = []; + const ownedIdentities = new Map(); + const recordOwned = (process: ProcessTableEntry): void => { + owned.push(process); + const identity = candidateIdentities.get(process.pid); + ownedIdentities.set( + process.pid, + identity?.state === "readable" ? identity.identity : null, + ); + }; + let bulkTokens: ReadonlyMap | undefined; + if (host.runTokens !== undefined) + bulkTokens = await host.runTokens(candidates); + const unreadable: UnreadableTokenCandidate[] = []; + for (const process of candidates) { + const bulkToken = bulkTokens?.get(process.pid); + if (bulkToken !== undefined) { + if (bulkToken.state === "readable") { + if (bulkToken.runId === runId) recordOwned(process); + continue; + } + unreadable.push({ process, diagnostic: bulkToken.reason }); + continue; + } + try { + if ((await host.environment(process.pid)).REA_PROCESS_RUN_ID === runId) + recordOwned(process); + } catch (cause: unknown) { + unreadable.push({ process, diagnostic: errorMessage(cause) }); + } + } + const remaining = await settleUnreadableTokenCandidates( + unreadable, + runId, + host, + recordOwned, + ); + const relatedPids = new Set(); + const relatedGroupIds = new Set(); + if (relation !== undefined) { + relatedPids.add(relation.leaderPid); + for (const descendant of descendantsOf(relation.leaderPid, processes)) + relatedPids.add(descendant.pid); + relatedGroupIds.add(relation.processGroupId); + for (const processGroupId of relation.sampledProcessGroupIds ?? []) + if (Number.isSafeInteger(processGroupId) && processGroupId > 0) + relatedGroupIds.add(processGroupId); + for (const process of owned) relatedGroupIds.add(process.processGroupId); + } + const unverified: OwnershipSweepUnverifiedProcess[] = []; + const strict: UnreadableTokenCandidate[] = []; + const withheld: UnreadableTokenCandidate[] = []; + for (const candidate of remaining) { + const isRelated = + relation === undefined || + relatedPids.has(candidate.process.pid) || + relatedGroupIds.has(candidate.process.processGroupId); + if ( + !isRelated && + environmentWithheldFromInspection(candidate.process, candidate.diagnostic) + ) { + withheld.push(candidate); + continue; + } + strict.push(candidate); + } + let finalLive: ReadonlySet | undefined; + try { + finalLive = new Set( + liveProcesses(await host.listProcesses()).map(({ pid }) => pid), + ); + } catch (cause: unknown) { + void cause; + finalLive = undefined; + } + for (const candidate of strict) { + if (finalLive !== undefined && !finalLive.has(candidate.process.pid)) + continue; + failures.push({ + pid: candidate.process.pid, + reason: "environment-unreadable", + diagnostic: candidate.diagnostic, + }); + } + for (const candidate of withheld) { + if (finalLive !== undefined && !finalLive.has(candidate.process.pid)) + continue; + unverified.push({ + pid: candidate.process.pid, + diagnostic: candidate.diagnostic, + }); + } + if (candidates.length > 0 && host.processIdentities !== undefined) { + const afterRead = await host.processIdentities(candidates); + const stillOwned: ProcessTableEntry[] = []; + const ownedPids = new Set(owned.map(({ pid }) => pid)); + for (const process of candidates) { + const before = candidateIdentities.get(process.pid); + const current = afterRead.get(process.pid); + if ( + before?.state === "readable" && + current?.state === "readable" && + current.identity === before.identity + ) { + if (ownedPids.has(process.pid)) stillOwned.push(process); + continue; + } + if (await processIsGone(host, process.pid)) continue; + failures.push({ + pid: process.pid, + reason: "process-identity-unavailable", + diagnostic: + "process identity changed or became unavailable during token validation", + }); + } + return { + owned: stillOwned, + failures, + unverified, + identities: ownedIdentities, + }; + } + return { owned, failures, unverified, identities: ownedIdentities }; +}; + +/** + * Re-read run tokens for unresolved sweep candidates so transient exec and + * exit states settle before classification. Each attempt revalidates + * liveness, refreshes the diagnostic, and is bounded by a fixed attempt + * budget; a still-unreadable candidate keeps its latest diagnostic. + */ +const settleUnreadableTokenCandidates = async ( + pending: readonly UnreadableTokenCandidate[], + runId: string, + host: ProcessOwnershipHost, + recordOwned: (process: ProcessTableEntry) => void, +): Promise => { + let remaining = pending; + for ( + let attempt = 0; + attempt < TOKEN_READ_RETRY_ATTEMPTS && remaining.length > 0; + attempt += 1 + ) { + await new Promise((resolve) => + setTimeout(resolve, TOKEN_READ_RETRY_DELAY_MS), + ); + let live: ReadonlySet | undefined; + try { + live = new Set( + liveProcesses(await host.listProcesses()).map(({ pid }) => pid), + ); + } catch (cause: unknown) { + void cause; + live = undefined; + } + let observations: + | ReadonlyMap + | undefined; + if (host.runTokens !== undefined) { + try { + observations = await host.runTokens( + remaining.map(({ process }) => process), + ); + } catch (cause: unknown) { + void cause; + observations = undefined; + } + } + const stillUnreadable: UnreadableTokenCandidate[] = []; + for (const candidate of remaining) { + const pid = candidate.process.pid; + if (live !== undefined && !live.has(pid)) continue; + const observation = + observations?.get(pid) ?? (await readEnvironmentRunToken(host, pid)); + if (observation.state === "readable") { + if (observation.runId === runId) recordOwned(candidate.process); + continue; + } + stillUnreadable.push({ + process: candidate.process, + diagnostic: observation.reason, + }); + } + remaining = stillUnreadable; + } + return remaining; +}; + +const readEnvironmentRunToken = async ( + host: ProcessOwnershipHost, + pid: number, +): Promise => { + try { + return { + state: "readable", + runId: (await host.environment(pid)).REA_PROCESS_RUN_ID, + }; + } catch (cause: unknown) { + return { state: "unavailable", reason: errorMessage(cause) }; + } +}; + +const processIsGone = async ( + host: ProcessOwnershipHost, + pid: number, +): Promise => { + try { + return !liveProcesses(await host.listProcesses()).some( + (process) => process.pid === pid, + ); + } catch (cause: unknown) { + void cause; + return false; + } }; diff --git a/src/process/ProcessOwnershipObservation.ts b/src/process/ProcessOwnershipObservation.ts index ee835dcb9b..d47af781c1 100644 --- a/src/process/ProcessOwnershipObservation.ts +++ b/src/process/ProcessOwnershipObservation.ts @@ -3,11 +3,14 @@ import { readFile } from "node:fs/promises"; import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js"; import { execFileOutput } from "./ExecFileOutput.js"; +import { createDarwinProcessRunTokenReader } from "./DarwinProcessRunTokenReader.js"; import type { OwnedProcessGroup, ProcessGroupObservation, ProcessLineageObservation, ProcessOwnershipHost, + ProcessIdentityObservation, + ProcessOwnershipBaseline, ProcessTableEntry, } from "./ProcessOwnership.js"; @@ -144,59 +147,237 @@ export const parseProcessEnvironment = ( }), ); +/** Read Linux stat field 22, tolerating spaces and closing parentheses in comm. */ +export const parseLinuxProcessStartTime = ( + value: string, +): string | undefined => { + const commandEnd = value.lastIndexOf(")"); + if (commandEnd < 0) return undefined; + const fields = value + .slice(commandEnd + 1) + .trim() + .split(/\s+/u); + const startTime = fields[19]; + return startTime !== undefined && /^\d+$/u.test(startTime) + ? startTime + : undefined; +}; + +const isExpectedAbort = ( + cause: unknown, + signal: AbortSignal | undefined, +): boolean => + signal?.aborted === true && + (cause === signal.reason || + (cause instanceof Error && cause.name === "AbortError")); + /** Create the operating-system process inspector for an explicit host context. */ export const createSystemProcessOwnershipHost = ( platform: NodeJS.Platform = process.platform, hostEnvironment: NodeJS.ProcessEnv = process.env, -): ProcessOwnershipHost => ({ - platform, - async listProcesses() { + options: { + /** Override the compiler executable used by the Darwin native reader. */ + readonly darwinXcrun?: string; + } = {}, +): ProcessOwnershipHost => { + const darwinTokens = + platform === "darwin" + ? createDarwinProcessRunTokenReader( + options.darwinXcrun === undefined + ? {} + : { xcrun: options.darwinXcrun }, + ) + : undefined; + const listProcesses = async (signal?: AbortSignal) => { if (platform === "win32") return []; const { stdout } = await execFileOutput( "ps", - ["-axo", "pid=,ppid=,pgid=,stat=,command="], - { env: hostEnvironment }, + ["-axo", "pid=,ppid=,pgid=,uid=,stat=,command="], + { env: hostEnvironment, ...(signal === undefined ? {} : { signal }) }, ); return stdout .split("\n") - .map((line) => /\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/u.exec(line)) + .map((line) => + /\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/u.exec(line), + ) .filter((match): match is RegExpExecArray => match !== null) .map((match) => ({ pid: Number(match[1]), parentPid: Number(match[2]), processGroupId: Number(match[3]), - state: match[4] ?? "", - command: match[5] ?? "", + uid: Number(match[4]), + state: match[5] ?? "", + command: match[6] ?? "", })); - }, - async environment(pid) { + }; + const processIdentities: NonNullable< + ProcessOwnershipHost["processIdentities"] + > = async (processes, signal) => { + if (processes.length === 0) return new Map(); if (platform === "linux") - return parseProcessEnvironment( - await readFile(`/proc/${pid}/environ`, "utf8"), + return new Map( + await Promise.all( + processes.map(async ({ pid }) => { + try { + const stat = await readFile(`/proc/${String(pid)}/stat`, { + encoding: "utf8", + ...(signal === undefined ? {} : { signal }), + }); + const identity = parseLinuxProcessStartTime(stat); + return [ + pid, + identity === undefined + ? ({ + state: "unavailable", + reason: "malformed_proc_stat", + } as const) + : ({ + state: "readable", + identity: `linux-starttime:${identity}`, + } as const), + ] as const; + } catch (cause: unknown) { + if (isExpectedAbort(cause, signal)) throw cause; + return [ + pid, + { state: "unavailable", reason: errorMessage(cause) } as const, + ] as const; + } + }), + ), + ); + if (darwinTokens === undefined) return new Map(); + try { + return await darwinTokens.identities(processes, signal); + } catch (cause: unknown) { + if (isExpectedAbort(cause, signal)) throw cause; + const reason = errorMessage(cause); + return new Map( + processes.map(({ pid }) => [ + pid, + { state: "unavailable", reason } as const, + ]), ); - const { stdout } = await execFileOutput("ps", ["eww", "-p", String(pid)], { - env: hostEnvironment, - }); - const observedEnvironment: Record = {}; - for (const match of stdout.matchAll( - /(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=([^\s]*)/gu, - )) { - const name = match[1]; - if (name !== undefined) observedEnvironment[name] = match[2] ?? ""; } - return observedEnvironment; - }, - signalGroup(processGroupId, signal) { - process.kill(-processGroupId, signal); - }, -}); + }; + const captureBaseline = async ( + signal?: AbortSignal, + ): Promise => { + if (platform !== "linux" && darwinTokens === undefined) return []; + const processes = liveProcesses(await listProcesses(signal)); + let identities = await processIdentities(processes, signal); + const unreadable = processes.filter( + ({ pid }) => identities.get(pid)?.state !== "readable", + ); + if (unreadable.length > 0) { + const stillLive = liveProcesses(await listProcesses(signal)).filter( + ({ pid }) => unreadable.some((entry) => entry.pid === pid), + ); + const retried = await processIdentities(stillLive, signal); + identities = new Map([...identities, ...retried]); + const unresolved = stillLive.filter( + ({ pid }) => identities.get(pid)?.state !== "readable", + ); + const liveAfterRetry = liveProcesses(await listProcesses(signal)); + const stillUnresolved = unresolved.filter(({ pid }) => + liveAfterRetry.some((entry) => entry.pid === pid), + ); + if (stillUnresolved.length > 0) + throw new Error( + `process identity snapshot is unavailable for ${String(stillUnresolved.length)} live processes`, + ); + } + return processes.map(({ pid }) => { + const observation = identities.get(pid); + return { + pid, + identity: + observation?.state === "readable" ? observation.identity : null, + }; + }); + }; + return { + platform, + prepare: async (signal) => { + await darwinTokens?.prepare(signal); + }, + listProcesses, + async environment(pid) { + if (platform === "linux") + return parseProcessEnvironment( + await readFile(`/proc/${pid}/environ`, "utf8"), + ); + if (platform === "darwin") { + const process = (await listProcesses()).find( + (entry) => entry.pid === pid, + ); + if (process === undefined) + throw new Error(`process ${String(pid)} is not live`); + const observation = (await darwinTokens?.read([process]))?.get(pid); + if (observation === undefined || observation.state === "unavailable") + throw new Error( + observation?.reason ?? "process run token could not be read", + ); + return observation.runId === undefined + ? {} + : { REA_PROCESS_RUN_ID: observation.runId }; + } + const { stdout } = await execFileOutput( + "ps", + ["eww", "-p", String(pid)], + { + env: hostEnvironment, + }, + ); + const observedEnvironment: Record = {}; + for (const match of stdout.matchAll( + /(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=([^\s]*)/gu, + )) { + const name = match[1]; + if (name !== undefined) observedEnvironment[name] = match[2] ?? ""; + } + return observedEnvironment; + }, + async runTokens(processes) { + if (platform !== "darwin" || processes.length === 0) return new Map(); + try { + return (await darwinTokens?.read(processes)) ?? new Map(); + } catch (cause: unknown) { + void cause; + return new Map( + processes.map(({ pid }) => [ + pid, + { state: "unavailable", reason: "reader_failure" } as const, + ]), + ); + } + }, + ...(platform !== "linux" && darwinTokens === undefined + ? {} + : { + processIdentities, + captureBaseline, + ...(darwinTokens === undefined ? {} : { close: darwinTokens.close }), + }), + signalGroup(processGroupId, signal) { + process.kill(-processGroupId, signal); + }, + }; +}; -const systemHost = createSystemProcessOwnershipHost(); +export const systemProcessOwnershipHost = createSystemProcessOwnershipHost(); + +/** Prepare native token inspection before REA launches a captured child. */ +export const prepareProcessOwnershipInspection = async ( + signal?: AbortSignal, +): Promise => { + await systemProcessOwnershipHost.prepare?.(signal); +}; /** Observe one group without signaling it, failing closed on identity doubt. */ export const observeOwnedProcessGroup = async ( ownership: OwnedProcessGroup, - host: ProcessOwnershipHost = systemHost, + host: ProcessOwnershipHost = systemProcessOwnershipHost, ): Promise => observeOwnedProcessGroupWithHost(ownership, host); @@ -209,7 +390,7 @@ export const observeOwnedProcessGroup = async ( */ export const observeOwnedProcessLineage = async ( ownership: OwnedProcessGroup, - host: ProcessOwnershipHost = systemHost, + host: ProcessOwnershipHost = systemProcessOwnershipHost, ): Promise => observeOwnedProcessLineageWithHost(ownership, host); diff --git a/src/process/ProviderProcess.ts b/src/process/ProviderProcess.ts index d41432d1b8..ef5ff82627 100644 --- a/src/process/ProviderProcess.ts +++ b/src/process/ProviderProcess.ts @@ -8,6 +8,7 @@ import type { OwnedProcessGroup, ProcessCleanupResult, } from "./ProcessOwnership.js"; +import { prepareProcessOwnershipInspection } from "./ProcessOwnershipObservation.js"; const DEFAULT_TERMINATION_GRACE_MS = 250; const DEFAULT_KILL_GRACE_MS = 1_000; @@ -29,6 +30,8 @@ export interface OwnedProviderProcessSpawnOptions { readonly hostEnvironment?: NodeJS.ProcessEnv; /** Opt into a writable protocol stream; other providers retain ignored stdin. */ readonly stdin?: "pipe"; + /** Cancels native ownership preparation before provider process creation. */ + readonly signal?: AbortSignal; } /** Spawned process paired with the identity proof required for group cleanup. */ @@ -69,6 +72,8 @@ export type ProviderProcessLaunch = /** Detached diagnostics captured for one supervised provider process. */ export interface ProviderProcessSnapshot { + /** True only when a configured diagnostic budget excluded output bytes. */ + readonly diagnosticTruncated?: boolean; readonly stdout: { readonly text: string; readonly bytes: number; @@ -88,6 +93,7 @@ export type ProviderProcessDiagnostic = readonly stream: "stdout" | "stderr"; readonly bytes: number; readonly totalBytes: number; + readonly truncated?: boolean; } | { readonly type: "exit"; @@ -101,6 +107,8 @@ export type ProviderProcessDiagnostic = export interface ProviderProcessSupervisorOptions { /** Retain stdout for diagnostics; disable when it carries a parsed protocol. Defaults to true. */ readonly captureStdout?: boolean; + /** Aggregate retained stdout/stderr byte budget; overflow is drained and reported, not retained. */ + readonly maxDiagnosticBytes?: number; readonly onDiagnostic?: (event: ProviderProcessDiagnostic) => void; } @@ -132,6 +140,9 @@ export type ProviderProcessStopResult = export const spawnOwnedProviderProcess = async ( options: OwnedProviderProcessSpawnOptions, ): Promise => { + options.signal?.throwIfAborted(); + await prepareProcessOwnershipInspection(options.signal); + options.signal?.throwIfAborted(); const platform = options.platform ?? process.platform; const hostEnvironment = options.hostEnvironment ?? process.env; const environment = { @@ -203,6 +214,9 @@ export class ProviderProcessSupervisor { }>(); readonly #outputClose = deferred(); #outputClosed = false; + #stdoutObservedBytes = 0; + #stderrObservedBytes = 0; + #diagnosticTruncated = false; readonly #onStdout = (chunk: Buffer | string): void => { this.#capture("stdout", this.#stdout, chunk); }; @@ -247,9 +261,10 @@ export class ProviderProcessSupervisor { this.#recordExit(launch.process.exitCode, launch.process.signalCode); } - /** Latest complete output and exit observation. */ + /** Latest retained output and exit observation, with explicit configured truncation. */ snapshot(): ProviderProcessSnapshot { return { + ...(this.#diagnosticTruncated ? { diagnosticTruncated: true } : {}), stdout: this.#stdout.snapshot(), stderr: this.#stderr.snapshot(), exitCode: this.#exitObservation?.code, @@ -261,6 +276,9 @@ export class ProviderProcessSupervisor { resetOutput(): void { this.#stdout.reset(); this.#stderr.reset(); + this.#stdoutObservedBytes = 0; + this.#stderrObservedBytes = 0; + this.#diagnosticTruncated = false; } /** Wait for process exit up to a caller-owned bounded interval. */ @@ -382,12 +400,29 @@ export class ProviderProcessSupervisor { chunk: Buffer | string, ): void { const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - capture.append(bytes); + if (stream === "stdout") this.#stdoutObservedBytes += bytes.byteLength; + else this.#stderrObservedBytes += bytes.byteLength; + const remaining = + this.#options.maxDiagnosticBytes === undefined + ? bytes.byteLength + : Math.max( + 0, + this.#options.maxDiagnosticBytes - + this.#stdout.bytes - + this.#stderr.bytes, + ); + const retained = Math.min(bytes.byteLength, remaining); + if (retained < bytes.byteLength) this.#diagnosticTruncated = true; + if (retained > 0) capture.append(bytes.subarray(0, retained)); this.#options.onDiagnostic?.({ type: "output", stream, bytes: bytes.byteLength, - totalBytes: capture.bytes, + totalBytes: + stream === "stdout" + ? this.#stdoutObservedBytes + : this.#stderrObservedBytes, + ...(retained < bytes.byteLength ? { truncated: true } : {}), }); } diff --git a/src/process/capture/FilesystemSnapshot.test.ts b/src/process/capture/FilesystemSnapshot.test.ts new file mode 100644 index 0000000000..b7e1410188 --- /dev/null +++ b/src/process/capture/FilesystemSnapshot.test.ts @@ -0,0 +1,127 @@ +import { fstatSync } from "node:fs"; +import { + mkdtemp, + lstat, + open, + readdir, + rm, + utimes, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { expect, it } from "vitest"; + +import { hashFile } from "./FilesystemSnapshot.js"; + +const countOpenDescriptorsFor = async (path: string): Promise => { + const directory = process.platform === "linux" ? "/proc/self/fd" : "/dev/fd"; + const expected = await lstat(path); + const descriptors = await readdir(directory); + const matches = await Promise.all( + descriptors.map(async (descriptor) => { + if (!/^\d+$/u.test(descriptor)) return false; + try { + const stats = fstatSync(Number(descriptor)); + return stats.dev === expected.dev && stats.ino === expected.ino; + } catch (cause: unknown) { + if (cause instanceof Error && "code" in cause && cause.code === "EBADF") + return false; + throw cause; + } + }), + ); + return matches.filter(Boolean).length; +}; + +it("does not hash a replacement file using the earlier path metadata", async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-identity-")); + const path = join(directory, "observed-file"); + try { + await writeFile(path, "captured file\n"); + const expected = await lstat(path); + await rm(path); + await writeFile(path, "replacement with a different identity and size\n"); + + await expect(hashFile(path, expected, 1_000)).resolves.toBeNull(); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +it("rejects a same-size rewrite even when its modification time is restored", async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-ctime-")); + const path = join(directory, "observed-file"); + try { + const fixedMtime = new Date("2020-01-02T03:04:05.000Z"); + await writeFile(path, "before\n"); + await utimes(path, fixedMtime, fixedMtime); + const expected = await lstat(path); + await expect + .poll(async () => { + await writeFile(path, "after!\n"); + await utimes(path, fixedMtime, fixedMtime); + return (await lstat(path)).ctimeMs; + }) + .not.toBe(expected.ctimeMs); + const rewritten = await lstat(path); + + expect(rewritten).toMatchObject({ + dev: expected.dev, + ino: expected.ino, + mode: expected.mode, + size: expected.size, + mtimeMs: expected.mtimeMs, + }); + expect(rewritten.ctimeMs).not.toBe(expected.ctimeMs); + + await expect(hashFile(path, expected, 1_000)).resolves.toBeNull(); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +it("rejects cancellation for an empty file before returning its digest", async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-empty-cancel-")); + const path = join(directory, "observed-file"); + try { + await writeFile(path, ""); + const expected = await lstat(path); + const controller = new AbortController(); + controller.abort(); + + await expect( + hashFile(path, expected, 1_000, controller.signal), + ).rejects.toMatchObject({ name: "AbortError" }); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +it.skipIf(process.platform === "win32")( + "closes the opened file when cancellation arrives during open", + async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-cancel-")); + const path = join(directory, "observed-file"); + try { + await writeFile(path, "captured file\n"); + const expected = await lstat(path); + const controller = new AbortController(); + const held = await open(path, "r"); + expect(await countOpenDescriptorsFor(path)).toBe(1); + await held.close(); + expect(await countOpenDescriptorsFor(path)).toBe(0); + const hashing = hashFile(path, expected, 1_000, controller.signal); + // hashFile reaches `open` synchronously before its first suspension, so + // this abort is observed after the descriptor is acquired and is handled + // by the `finally` close path. + controller.abort(); + + await expect(hashing).rejects.toMatchObject({ name: "AbortError" }); + expect(await countOpenDescriptorsFor(path)).toBe(0); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }, +); diff --git a/src/application/FilesystemSnapshot.ts b/src/process/capture/FilesystemSnapshot.ts similarity index 81% rename from src/application/FilesystemSnapshot.ts rename to src/process/capture/FilesystemSnapshot.ts index 246497d4a5..aaf15fda82 100644 --- a/src/application/FilesystemSnapshot.ts +++ b/src/process/capture/FilesystemSnapshot.ts @@ -2,7 +2,10 @@ import { createHash } from "node:crypto"; import { constants as fsConstants } from "node:fs"; import { lstat, open, readdir, readlink } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; -import type { FileState, ProcessScenario } from "../domain/processCapture.js"; +import type { + FileState, + ProcessScenario, +} from "../../domain/process/processCapture.js"; import type { Stats } from "node:fs"; export interface SnapshotResult { @@ -19,6 +22,16 @@ const hasSameIdentity = ( before.ino === after.ino && (expected === "directory" ? after.isDirectory() : after.isSymbolicLink()); +const hasSameFileState = (before: Stats, after: Stats): boolean => + before.dev === after.dev && + before.ino === after.ino && + before.isFile() && + after.isFile() && + before.mode === after.mode && + before.size === after.size && + before.mtimeMs === after.mtimeMs && + before.ctimeMs === after.ctimeMs; + const lstatIfPresent = async (path: string): Promise => { try { return await lstat(path); @@ -29,18 +42,26 @@ const lstatIfPresent = async (path: string): Promise => { } }; -const hashFile = async ( +/** Hash a file only when its opened descriptor still matches the captured path state. */ +export const hashFile = async ( path: string, + expected: Stats, maxBytes: number, signal?: AbortSignal, ): Promise => { + signal?.throwIfAborted(); const handle = await open( path, - fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0), + fsConstants.O_RDONLY | + (fsConstants.O_NOFOLLOW ?? 0) | + (fsConstants.O_NONBLOCK ?? 0), ); try { + signal?.throwIfAborted(); const stats = await handle.stat(); - if (!stats.isFile() || stats.size > maxBytes) return null; + signal?.throwIfAborted(); + if (!hasSameFileState(expected, stats) || stats.size > maxBytes) + return null; const hash = createHash("sha256"); const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes)); let position = 0; @@ -57,8 +78,8 @@ const hashFile = async ( position += bytesRead; } const after = await handle.stat(); - if (after.size !== stats.size || after.mtimeMs !== stats.mtimeMs) - return null; + signal?.throwIfAborted(); + if (!hasSameFileState(stats, after)) return null; return hash.digest("hex"); } finally { await handle.close(); @@ -110,7 +131,7 @@ export const snapshotRoots = async ( if (stats.isFile()) { const sha256 = remainingBytes >= stats.size - ? await hashFile(path, remainingBytes, signal) + ? await hashFile(path, stats, remainingBytes, signal) : null; remainingBytes -= sha256 === null ? 0 : stats.size; if (sha256 === null) truncated = true; diff --git a/src/application/ProcessCaptureCapability.ts b/src/process/capture/ProcessCaptureCapability.ts similarity index 86% rename from src/application/ProcessCaptureCapability.ts rename to src/process/capture/ProcessCaptureCapability.ts index e7ec0ef3ac..7d0de76251 100644 --- a/src/application/ProcessCaptureCapability.ts +++ b/src/process/capture/ProcessCaptureCapability.ts @@ -1,4 +1,5 @@ import { tmpdir } from "node:os"; +import { prepareProcessOwnershipInspection } from "../ProcessOwnershipObservation.js"; export type ProcessCaptureCapability = | { readonly available: true; readonly backend: "node-pty" } @@ -20,6 +21,7 @@ export const processCaptureOwnershipUnavailableReason = ( export interface ProcessCaptureProbeOptions { readonly platform?: NodeJS.Platform; readonly loadPty?: () => Promise; + readonly prepareOwnershipInspector?: () => Promise; } /** Probe the native PTY seam and explain loader failures at their source. */ @@ -56,6 +58,22 @@ export const probeProcessCaptureCapability = async ( backend: "node-pty", reason: ownershipReason, }; + if (platform === "darwin") { + try { + await ( + options.prepareOwnershipInspector ?? prepareProcessOwnershipInspection + )(); + } catch (cause: unknown) { + return { + available: false, + backend: "node-pty", + reason: + cause instanceof Error + ? cause.message + : "macOS process ownership inspection could not be prepared", + }; + } + } try { const { spawn } = await ( options.loadPty ?? (() => import("@lydell/node-pty")) diff --git a/src/application/ProcessCaptureEnvironment.ts b/src/process/capture/ProcessCaptureEnvironment.ts similarity index 90% rename from src/application/ProcessCaptureEnvironment.ts rename to src/process/capture/ProcessCaptureEnvironment.ts index 1414349cdb..af4d592f6b 100644 --- a/src/application/ProcessCaptureEnvironment.ts +++ b/src/process/capture/ProcessCaptureEnvironment.ts @@ -1,4 +1,4 @@ -import type { ProcessScenario } from "../domain/processCapture.js"; +import type { ProcessScenario } from "../../domain/process/processCapture.js"; interface ProcessCaptureEnvironmentOptions { readonly scenario: ProcessScenario; diff --git a/src/application/ProcessCaptureError.test.ts b/src/process/capture/ProcessCaptureError.test.ts similarity index 92% rename from src/application/ProcessCaptureError.test.ts rename to src/process/capture/ProcessCaptureError.test.ts index 3dedc42600..d554d8ee89 100644 --- a/src/application/ProcessCaptureError.test.ts +++ b/src/process/capture/ProcessCaptureError.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { ProcessCaptureError, processCaptureCancelled, diff --git a/src/application/ProcessCaptureError.ts b/src/process/capture/ProcessCaptureError.ts similarity index 56% rename from src/application/ProcessCaptureError.ts rename to src/process/capture/ProcessCaptureError.ts index da57679abe..4f24225594 100644 --- a/src/application/ProcessCaptureError.ts +++ b/src/process/capture/ProcessCaptureError.ts @@ -1,10 +1,17 @@ -import { AnalysisError } from "../domain/analysisErrorBase.js"; +import { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { + PartialProcessCaptureObservation, + ProcessCaptureCleanupReport, +} from "../../domain/process/processCapture.js"; interface ProcessCaptureErrorOptions extends ErrorOptions { readonly userMessage?: string; readonly userCategory?: "cancelled"; readonly reason?: "capture_failed" | "cleanup_incomplete" | "cancelled"; readonly cleanupResources?: readonly string[]; + readonly executionFailure?: string; + readonly partialObservation?: PartialProcessCaptureObservation; + readonly cleanupReport?: ProcessCaptureCleanupReport; } /** Typed application failure produced by controlled process capture. */ @@ -16,6 +23,11 @@ export class ProcessCaptureError extends AnalysisError { readonly reason: NonNullable; override readonly cleanupIncomplete: boolean; override readonly cleanupResources: readonly string[]; + override readonly executionFailure: string | undefined; + override readonly partialObservation: + | PartialProcessCaptureObservation + | undefined; + override readonly cleanupReport: ProcessCaptureCleanupReport | undefined; constructor(message: string, options?: ProcessCaptureErrorOptions) { super(message, options); @@ -26,9 +38,22 @@ export class ProcessCaptureError extends AnalysisError { (options?.userCategory === "cancelled" ? "cancelled" : "capture_failed"); this.cleanupIncomplete = this.reason === "cleanup_incomplete"; this.cleanupResources = options?.cleanupResources ?? []; + this.executionFailure = options?.executionFailure; + this.partialObservation = options?.partialObservation; + this.cleanupReport = options?.cleanupReport; } } +/** Return one non-recursive, domain-neutral summary of an execution failure. */ +export const describeProcessCaptureExecutionFailure = ( + failure: unknown, +): string | undefined => { + if (failure instanceof Error) return failure.message || failure.name; + if (typeof failure === "string") return failure; + if (failure === undefined) return undefined; + return "Process capture failed with a non-error rejection."; +}; + /** Caller-visible cancellation without exposing capture implementation state. */ export const processCaptureCancelled = (): ProcessCaptureError => new ProcessCaptureError("process capture was cancelled", { diff --git a/src/application/ProcessCaptureJournal.test.ts b/src/process/capture/ProcessCaptureJournal.test.ts similarity index 100% rename from src/application/ProcessCaptureJournal.test.ts rename to src/process/capture/ProcessCaptureJournal.test.ts diff --git a/src/application/ProcessCaptureJournal.ts b/src/process/capture/ProcessCaptureJournal.ts similarity index 98% rename from src/application/ProcessCaptureJournal.ts rename to src/process/capture/ProcessCaptureJournal.ts index a40f11ac36..027d193c47 100644 --- a/src/application/ProcessCaptureJournal.ts +++ b/src/process/capture/ProcessCaptureJournal.ts @@ -5,7 +5,7 @@ import type { ProcessCaptureEventJournalEntry, ProcessScenario, RecordProcessCaptureEvent, -} from "../domain/processCapture.js"; +} from "../../domain/process/processCapture.js"; import { normalizeProcessElapsedTime, normalizeProcessText, diff --git a/src/process/capture/ProcessCaptureLifecycle.ts b/src/process/capture/ProcessCaptureLifecycle.ts new file mode 100644 index 0000000000..50f9d8e1d5 --- /dev/null +++ b/src/process/capture/ProcessCaptureLifecycle.ts @@ -0,0 +1,999 @@ +import { createHash, randomUUID } from "node:crypto"; +import { constants, statSync, type BigIntStats } from "node:fs"; +import { open, mkdtemp, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { IPty } from "@lydell/node-pty"; + +import type { + FilesystemCheckpoint, + InteractionEvent, + ProcessCapture, + UnverifiedProcessCapture, + ProcessSample, + ProcessSettlement, + ProcessCaptureCleanupReport, + PartialProcessCaptureObservation, + IncompleteProcessCaptureObservations, + PartialFilesystemSnapshot, + PartialProcessObservationField, + ProcessScenario, + ProcessCaptureEventJournalEntry, + RecordProcessCaptureEvent, + TerminalFrame, +} from "../../domain/process/processCapture.js"; +import { + digestProcessCommitment, + parseProcessCapture, + partialProcessCaptureObservationSchema, + processComparisonContract, + processScenarioCommitment, +} from "../../domain/process/processCapture.js"; +import { PRODUCT_IDENTITY } from "../../identity.js"; +import type { SnapshotResult } from "./FilesystemSnapshot.js"; +import { snapshotRoots } from "./FilesystemSnapshot.js"; +import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js"; +import { + cleanupOwnedProcessGroup, + verifyNoTokenOwnedProcesses, + type ProcessCleanupResult, + type ProcessOwnershipBaseline, +} from "../ProcessOwnership.js"; +import { + observeOwnedProcessGroup, + prepareProcessOwnershipInspection, + systemProcessOwnershipHost, +} from "../ProcessOwnershipObservation.js"; +import { + describeProcessCaptureExecutionFailure, + ProcessCaptureError, +} from "./ProcessCaptureError.js"; +import { assertNotCancelled } from "./ProcessScenarioRuntimeValidation.js"; +import { + normalizeProcessElapsedTime, + normalizeProcessSamples, + normalizeProcessText, +} from "./ProcessNormalization.js"; +import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js"; +import { TerminalRenderer } from "./TerminalRenderer.js"; +import { scheduleProcessInterval, type ProcessTimer } from "./ProcessTimer.js"; + +interface TerminalExitOptions { + readonly terminal: IPty; + readonly scenario: ProcessScenario; + readonly started: number; + readonly lastOutput: () => number; + readonly signal: AbortSignal | undefined; + readonly timers: Set; + readonly interactions: InteractionEvent[]; + readonly dispatchedEventIndexes: ReadonlySet; + readonly recordEvent: RecordProcessCaptureEvent; +} + +interface CaptureResultOptions { + readonly frames: readonly TerminalFrame[]; + readonly exit: { + readonly exitCode: number; + readonly signal?: number; + readonly reason: "exited" | "timeout" | "idle_timeout"; + }; + readonly samples: readonly ProcessSample[]; + readonly before: SnapshotResult; + readonly after: SnapshotResult; + readonly truncated: boolean; + readonly scenario: ProcessScenario; + readonly rootPid: number; + readonly samplingPartial: boolean; + readonly renderedFrames: UnverifiedProcessCapture["rendered_frames"]; + readonly interactions: readonly InteractionEvent[]; + readonly checkpoints: readonly FilesystemCheckpoint[]; + readonly settlement: ObservedProcessSettlement; + readonly manifest: UnverifiedProcessCapture["manifest"]; + readonly eventJournal: readonly ProcessCaptureEventJournalEntry[]; +} + +/** Settlement observation before process-resource cleanup has completed. */ +export type ObservedProcessSettlement = + | Pick< + Extract, + "state" | "elapsed_ms" + > + | Pick< + Exclude, + "state" | "elapsed_ms" + >; + +/** Capture observations before owned-resource cleanup has been verified. */ +export type PendingProcessCapture = Omit< + UnverifiedProcessCapture, + "cleanup" | "settlement" +> & { + readonly settlement: ObservedProcessSettlement; +}; + +/** Mutable collection of facts available if completion stops before a capture exists. */ +export interface ProcessCaptureObservationBuffer { + target_pid: PartialProcessObservationField; + frames: PartialProcessObservationField; + rendered_frames: PartialProcessObservationField< + UnverifiedProcessCapture["rendered_frames"] + >; + interaction_events: PartialProcessObservationField< + UnverifiedProcessCapture["interaction_events"] + >; + exit: IncompleteProcessCaptureObservations["exit"]; + settlement: IncompleteProcessCaptureObservations["settlement"]; + process_samples: PartialProcessObservationField; + filesystem_snapshots: { + before: PartialProcessObservationField; + after: PartialProcessObservationField; + }; + event_journal: PartialProcessObservationField< + readonly ProcessCaptureEventJournalEntry[] + >; + manifest: IncompleteProcessCaptureObservations["manifest"]; +} + +/** Seed explicit observation availability before capture completion begins. */ +export const createProcessCaptureObservationBuffer = (options: { + readonly frames: readonly TerminalFrame[]; + readonly interactions: readonly InteractionEvent[]; + readonly samples: readonly ProcessSample[]; + readonly eventJournal: readonly ProcessCaptureEventJournalEntry[]; + readonly before: SnapshotResult; +}): ProcessCaptureObservationBuffer => ({ + target_pid: { + state: "unavailable", + reason: "No PTY process was spawned before the run failed.", + }, + frames: { state: "available", value: options.frames }, + rendered_frames: { + state: "unavailable", + reason: + "Rendered terminal frames were not collected before capture completion finished.", + }, + interaction_events: { state: "available", value: options.interactions }, + exit: { + state: "unavailable", + reason: "Terminal exit was not observed before the run failed.", + }, + settlement: { + state: "unavailable", + reason: + "Process settlement was not observed before capture completion finished.", + }, + process_samples: { state: "available", value: options.samples }, + filesystem_snapshots: { + before: { + state: "available", + value: { + files: options.before.files, + truncated: options.before.truncated, + }, + }, + after: { + state: "unavailable", + reason: + "Final filesystem snapshot was not collected before capture completion finished.", + }, + }, + event_journal: { state: "available", value: options.eventJournal }, + manifest: { + state: "unavailable", + reason: + "Capture manifest was not produced before capture completion finished.", + }, +}); + +const normalizePartialProcessObservations = ( + observations: IncompleteProcessCaptureObservations, + context: { readonly scenario: ProcessScenario; readonly rootPid?: number }, +): IncompleteProcessCaptureObservations => { + const targetPid = observations.target_pid; + const target_pid = + targetPid.state === "available" && context.rootPid !== undefined + ? { + state: "available" as const, + value: context.scenario.normalization.pids ? 1 : context.rootPid, + } + : targetPid; + const samples = observations.process_samples; + const process_samples = + samples.state === "available" && context.rootPid !== undefined + ? { + state: "available" as const, + value: normalizeProcessSamples( + samples.value, + context.scenario, + context.rootPid, + ), + } + : samples; + return { ...observations, target_pid, process_samples }; +}; + +/** Wait for trailing PTY callbacks before the capture becomes immutable. */ +export const settleProcessCaptureJournal = async ( + journal: readonly ProcessCaptureEventJournalEntry[], + quietMs = 25, + maxWaitMs = 500, +): Promise => { + const deadline = Date.now() + maxWaitMs; + let observed = journal.length; + let lastChangeAt = Date.now(); + while (Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 4)); + if (journal.length !== observed) { + observed = journal.length; + lastChangeAt = Date.now(); + } else if (Date.now() - lastChangeAt >= quietMs) return; + } +}; + +export const buildCaptureResult = ( + options: CaptureResultOptions, +): PendingProcessCapture => { + const hasSensitiveScriptedInput = options.scenario.events.some( + (event) => event.type === "input" && event.sensitive, + ); + const sensitiveInputUnknown = + "Sensitive scripted input values are redacted from process capture Evidence."; + const filesystemObservationUnknown = + "No filesystem observation paths were selected; filesystem effects remain unknown."; + const hasFilesystemObservations = + options.scenario.filesystem_observation_paths.length > 0; + return { + manifest: options.manifest, + normalization: options.scenario.normalization, + frames: options.frames, + rendered_frames: options.renderedFrames, + interaction_events: options.interactions, + exit: { + code: + options.exit.reason === "exited" && options.exit.exitCode >= 0 + ? options.exit.exitCode + : null, + signal: options.exit.signal ?? null, + reason: options.exit.reason, + }, + settlement: options.settlement, + process_samples: normalizeProcessSamples( + options.samples, + options.scenario, + options.rootPid, + ), + filesystem_checkpoints: options.checkpoints, + event_journal: options.eventJournal, + files_before: options.before.files, + files_after: options.after.files, + filesystem_effects: classifyFilesystemEffects( + options.before.files, + options.after.files, + ), + truncated: options.truncated, + limitations: [ + "The executable digest is a prelaunch file sample; matching path metadata immediately after spawn does not prove an atomic operating-system image binding.", + "Process trees are sampled and may omit short-lived descendants.", + ...(options.samplingPartial + ? ["Process-tree sampling ended with an incomplete observation."] + : []), + "Filesystem observations are before/after snapshots, not syscall traces.", + "Inherited host environment variables are not recorded and may affect results.", + ...(!hasFilesystemObservations ? [filesystemObservationUnknown] : []), + ...(hasSensitiveScriptedInput ? [sensitiveInputUnknown] : []), + ], + residual_unknowns: [ + { + scope: "process", + reason: + "Process trees are sampled and may omit short-lived descendants.", + }, + { + scope: "process", + reason: + "The executable digest is a prelaunch file sample; operating-system image binding is not atomically observed.", + }, + { + scope: "environment", + reason: "Inherited host environment variables are not recorded.", + }, + { + scope: "network", + reason: "Network activity is not observed by this capture.", + }, + ...(!hasFilesystemObservations + ? [ + { + scope: "filesystem" as const, + reason: filesystemObservationUnknown, + }, + ] + : []), + ...(hasSensitiveScriptedInput + ? [{ scope: "interaction" as const, reason: sensitiveInputUnknown }] + : []), + ], + }; +}; + +interface ExecutableFileIdentity { + readonly dev: bigint; + readonly ino: bigint; + readonly mode: bigint; + readonly size: bigint; + readonly mtimeNs: bigint; + readonly ctimeNs: bigint; +} + +export interface SelectedExecutableObservation { + readonly sha256: string | null; + readonly identity: ExecutableFileIdentity | null; + readonly reason: string | null; +} + +export interface LaunchedExecutableObservation { + readonly selectedSha256: string | null; + readonly sha256: string | null; + readonly state: "path_metadata_unchanged" | "unknown"; + readonly reason: string | null; +} + +const identityFromStats = (stats: BigIntStats): ExecutableFileIdentity => ({ + dev: stats.dev, + ino: stats.ino, + mode: stats.mode, + size: stats.size, + mtimeNs: stats.mtimeNs, + ctimeNs: stats.ctimeNs, +}); + +const sameExecutableIdentity = ( + left: ExecutableFileIdentity, + right: ExecutableFileIdentity, +): boolean => + left.dev === right.dev && + left.ino === right.ino && + left.mode === right.mode && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs; + +const filesystemFailure = (cause: unknown): string | undefined => { + if (!(cause instanceof Error) || !("code" in cause) || !("syscall" in cause)) + return undefined; + return `Executable file observation failed: ${cause.message}`; +}; + +/** Metadata IO seam for deterministic prelaunch cancellation and identity checks. */ +export interface SelectedExecutableFileSystem { + open(path: string, flags: number): ReturnType; + stat(path: string): Promise; +} + +const selectedExecutableFileSystem: SelectedExecutableFileSystem = { + open, + stat: (path) => stat(path, { bigint: true }), +}; + +/** Sample the selected file without launching it; retain cancellation and file identity. */ +export const observeSelectedExecutable = async ( + path: string, + signal?: AbortSignal, + fileSystem: SelectedExecutableFileSystem = selectedExecutableFileSystem, +): Promise => { + assertNotCancelled(signal); + let handle: Awaited> | undefined; + try { + handle = await fileSystem.open( + path, + constants.O_RDONLY | constants.O_NONBLOCK, + ); + const openedStats = await handle.stat({ bigint: true }); + if (!openedStats.isFile()) + return { + sha256: null, + identity: null, + reason: "Selected executable is not a regular file.", + }; + const openedIdentity = identityFromStats(openedStats); + const selectedPathIdentity = identityFromStats(await fileSystem.stat(path)); + if (!sameExecutableIdentity(openedIdentity, selectedPathIdentity)) + return { + sha256: null, + identity: null, + reason: "Selected executable path changed while it was being opened.", + }; + + // IO above can settle after cancellation. Do not construct a stream with an + // already-aborted signal: Node can emit a second, unhandled AbortError. + assertNotCancelled(signal); + const hash = createHash("sha256"); + for await (const chunk of handle.createReadStream({ + autoClose: false, + signal, + })) { + assertNotCancelled(signal); + hash.update(chunk); + } + assertNotCancelled(signal); + const finalIdentity = identityFromStats( + await handle.stat({ bigint: true }), + ); + if (!sameExecutableIdentity(openedIdentity, finalIdentity)) + return { + sha256: null, + identity: null, + reason: "Selected executable changed while its contents were sampled.", + }; + return { + sha256: hash.digest("hex"), + identity: openedIdentity, + reason: null, + }; + } catch (cause: unknown) { + assertNotCancelled(signal); + const reason = filesystemFailure(cause); + if (reason !== undefined) return { sha256: null, identity: null, reason }; + throw cause; + } finally { + await handle?.close(); + } +}; + +/** Compare the selected file metadata with the path immediately after PTY spawn. */ +export const observeLaunchedExecutable = ( + path: string, + selected: SelectedExecutableObservation, +): LaunchedExecutableObservation => { + if (selected.identity === null || selected.sha256 === null) + return { + selectedSha256: selected.sha256, + sha256: null, + state: "unknown", + reason: selected.reason ?? "Selected executable identity is unavailable.", + }; + try { + const currentStats = statSync(path, { bigint: true }); + const currentIdentity = identityFromStats(currentStats); + if ( + currentStats.isFile() && + sameExecutableIdentity(selected.identity, currentIdentity) + ) + return { + selectedSha256: selected.sha256, + sha256: selected.sha256, + state: "path_metadata_unchanged", + reason: null, + }; + return { + selectedSha256: selected.sha256, + sha256: null, + state: "unknown", + reason: "Executable path metadata changed between sampling and spawn.", + }; + } catch (cause: unknown) { + const reason = filesystemFailure(cause); + if (reason !== undefined) + return { + selectedSha256: selected.sha256, + sha256: null, + state: "unknown", + reason, + }; + throw cause; + } +}; + +export const createRunManifest = async ( + scenario: ProcessScenario, + startedAt: Date, + completedAt: Date, + executable: LaunchedExecutableObservation, + host: { + readonly platform: NodeJS.Platform; + readonly architecture: NodeJS.Architecture; + } = { + platform: process.platform, + architecture: process.arch, + }, +): Promise => { + const scenarioCommitment = processScenarioCommitment( + scenario, + executable.selectedSha256 ?? undefined, + ); + const comparisonContract = processComparisonContract(scenario); + return { + rea_version: PRODUCT_IDENTITY.packageVersion, + provider_version: PROCESS_PROVIDER.version, + platform: host.platform, + architecture: host.architecture, + pty_backend: "node-pty", + started_at: startedAt.toISOString(), + completed_at: completedAt.toISOString(), + scenario: scenarioCommitment, + comparison_contract: comparisonContract, + full_scenario_sha256: digestProcessCommitment(scenarioCommitment), + comparison_contract_sha256: digestProcessCommitment(comparisonContract), + selected_executable_sha256: executable.selectedSha256, + executable_sha256: executable.sha256, + executable_identity: { + state: executable.state, + reason: executable.reason, + }, + normalization_sha256: digestProcessCommitment(scenario.normalization), + }; +}; + +export const observeSettlement = async ( + runId: string, + processGroupIds: readonly number[], + settleMs: number, + recordEvent: RecordProcessCaptureEvent = () => undefined, + platform: NodeJS.Platform = process.platform, +): Promise => { + if (platform === "win32") { + recordEvent("lifecycle", 1); + return { state: "unverifiable", elapsed_ms: 0 }; + } + const started = Date.now(); + let consecutiveEmpty = 0; + let deadlineReached = false; + while (!deadlineReached) { + const observations = await Promise.all( + [...new Set(processGroupIds)].map((processGroupId) => + observeOwnedProcessGroup({ + runId, + leaderPid: processGroupId, + processGroupId, + }), + ), + ); + if (observations.some(({ state }) => state === "unverifiable")) { + recordEvent("lifecycle", 1); + return { state: "unverifiable", elapsed_ms: Date.now() - started }; + } + if (observations.every(({ state }) => state === "empty")) { + consecutiveEmpty += 1; + if (consecutiveEmpty >= 2) { + recordEvent("lifecycle", 1); + return { state: "quiesced", elapsed_ms: Date.now() - started }; + } + } else consecutiveEmpty = 0; + deadlineReached = Date.now() - started >= settleMs; + if (deadlineReached) break; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + recordEvent("lifecycle", 1); + return { state: "alive_at_deadline", elapsed_ms: Date.now() - started }; +}; + +export const awaitTerminalExit = async ({ + terminal, + scenario, + started, + lastOutput, + signal, + timers, + interactions, + dispatchedEventIndexes, + recordEvent, +}: TerminalExitOptions): Promise<{ + exitCode: number; + signal?: number; + reason: "exited" | "timeout" | "idle_timeout" | "cancelled"; +}> => + new Promise((resolveExit) => { + // The kill caused by a deadline is observed later as an ordinary PTY exit. + // Keep the initiating lifecycle reason so comparisons distinguish a target + // exit from harness-owned timeout, idle-timeout, and cancellation cleanup. + let reason: "exited" | "timeout" | "idle_timeout" | "cancelled" = "exited"; + terminal.onExit((exit) => { + recordEvent("lifecycle", 0); + for (const [eventIndex, event] of scenario.events.entries()) { + if (dispatchedEventIndexes.has(eventIndex)) continue; + const sequence = interactions.length; + interactions.push({ + sequence, + scheduled_at_ms: event.at_ms, + dispatched_at_ms: Math.max(0, Date.now() - started), + type: event.type, + data: + event.type === "input" + ? "" + : event.type === "resize" + ? `${String(event.columns)}x${String(event.rows)}` + : event.signal, + outcome: "target_exited", + }); + recordEvent("interaction_events", sequence); + } + for (const timer of timers) { + timer.cancel(); + } + timers.clear(); + resolveExit({ ...exit, reason }); + }); + const timeout = scheduleProcessInterval(() => { + if (signal?.aborted === true) { + reason = "cancelled"; + terminal.kill("SIGKILL"); + } else if (Date.now() - started >= scenario.timeout_ms) { + reason = "timeout"; + terminal.kill("SIGKILL"); + } else if (Date.now() - lastOutput() >= scenario.idle_timeout_ms) { + reason = "idle_timeout"; + terminal.kill("SIGKILL"); + } + }, 20); + timers.add(timeout); + }); + +/** Host operations used when releasing one captured process run. */ +export interface ProcessCaptureCleanupHost { + readonly platform: NodeJS.Platform; + readonly cleanupProcessGroup: typeof cleanupOwnedProcessGroup; + readonly verifyTokenOwnedProcesses: typeof verifyNoTokenOwnedProcesses; + readonly removeTemporaryRoot: (path: string) => Promise; +} + +const processCaptureCleanupHost: ProcessCaptureCleanupHost = { + platform: process.platform, + cleanupProcessGroup: cleanupOwnedProcessGroup, + verifyTokenOwnedProcesses: verifyNoTokenOwnedProcesses, + removeTemporaryRoot: (path) => rm(path, { recursive: true, force: true }), +}; + +const PROCESS_CLEANUP_VERIFICATION_GRACE_MS = 1_000; +const PROCESS_CLEANUP_VERIFICATION_INTERVAL_MS = 25; + +const verifyTokenOwnedProcessesUntilSettled = async ( + verify: () => Promise, +): Promise => { + const deadline = Date.now() + PROCESS_CLEANUP_VERIFICATION_GRACE_MS; + let result = await verify(); + while (!result.cleaned) { + const remaining = deadline - Date.now(); + if (remaining <= 0) return result; + await new Promise((resolve) => + setTimeout( + resolve, + Math.min(PROCESS_CLEANUP_VERIFICATION_INTERVAL_MS, remaining), + ), + ); + result = await verify(); + } + return result; +}; + +export const releaseProcessResources = async (options: { + readonly timers: ReadonlySet; + readonly terminal: Pick | undefined; + readonly renderer: TerminalRenderer | undefined; + readonly runId: string; + readonly temporaryRoot: string; + readonly captureBaseline?: ProcessOwnershipBaseline; + readonly sampledProcessGroupIds?: readonly number[]; + readonly host?: ProcessCaptureCleanupHost; +}): Promise => { + const host = options.host ?? processCaptureCleanupHost; + for (const timer of options.timers) timer.cancel(); + let terminalRenderer: ProcessCaptureCleanupReport["terminal_renderer"] = { + state: options.renderer === undefined ? "not_required" : "cleaned", + reason: null, + }; + try { + await options.renderer?.dispose(); + } catch (cause: unknown) { + terminalRenderer = { + state: "failed", + reason: + cause instanceof Error + ? cause.message || cause.name || "renderer dispose failed" + : "renderer dispose failed", + }; + } + let ownedProcessGroup: ProcessCaptureCleanupReport["owned_process_group"] = { + state: options.terminal === undefined ? "not_required" : "cleaned", + reason: null, + }; + if (options.terminal !== undefined) { + if (host.platform === "win32") { + ownedProcessGroup = { + state: "unverified", + reason: + "owned process cleanup is unverifiable on Windows without process-job authority", + }; + } else { + const terminalPid = options.terminal.pid; + const relation = { + leaderPid: terminalPid, + processGroupId: terminalPid, + ...(options.sampledProcessGroupIds === undefined + ? {} + : { sampledProcessGroupIds: options.sampledProcessGroupIds }), + }; + const cleaned = await host.cleanupProcessGroup({ + runId: options.runId, + leaderPid: terminalPid, + processGroupId: terminalPid, + sweepTokenOwnedProcesses: true, + ...(options.sampledProcessGroupIds === undefined + ? {} + : { sampledProcessGroupIds: options.sampledProcessGroupIds }), + ...(options.captureBaseline === undefined + ? {} + : { captureBaseline: options.captureBaseline }), + }); + const unverified = [...(cleaned.unverified ?? [])]; + if (!cleaned.cleaned) { + ownedProcessGroup = { state: "unverified", reason: cleaned.reason }; + } else { + const verified = await verifyTokenOwnedProcessesUntilSettled(() => + host.verifyTokenOwnedProcesses( + options.runId, + undefined, + options.captureBaseline, + relation, + ), + ); + unverified.push(...(verified.unverified ?? [])); + if (!verified.cleaned) + ownedProcessGroup = { state: "unverified", reason: verified.reason }; + } + if (unverified.length > 0) + ownedProcessGroup = { + ...ownedProcessGroup, + unverified_processes: [ + ...new Map( + unverified.map(({ pid, diagnostic }) => [ + `${String(pid)}:${diagnostic}`, + { pid, reason: diagnostic }, + ]), + ).values(), + ], + }; + } + } + let temporaryRoot: ProcessCaptureCleanupReport["temporary_root"] = { + state: "cleaned", + reason: null, + }; + try { + await host.removeTemporaryRoot(options.temporaryRoot); + } catch (cause: unknown) { + temporaryRoot = { + state: "failed", + reason: + cause instanceof Error + ? cause.message || cause.name || "temporary root removal failed" + : "temporary root removal failed", + }; + } + return { + owned_process_group: ownedProcessGroup, + terminal_renderer: terminalRenderer, + temporary_root: temporaryRoot, + }; +}; + +export const cleanupReportFailure = ( + report: ProcessCaptureCleanupReport, +): string | undefined => { + const failures = Object.entries(report).filter( + ([, outcome]) => + outcome.state === "failed" || outcome.state === "unverified", + ); + return failures.length === 0 + ? undefined + : failures + .map( + ([resource, outcome]) => + `${resource}: ${outcome.reason ?? outcome.state}`, + ) + .join("; "); +}; + +export const captureTerminalFrames = (options: { + readonly terminal: IPty; + readonly scenario: ProcessScenario; + readonly frames: TerminalFrame[]; + readonly started: number; + readonly temporaryRoot: string; + readonly onOutput: () => void; + readonly renderer: TerminalRenderer; + readonly recordEvent: RecordProcessCaptureEvent; +}): (() => boolean) => { + let outputBytes = 0; + let truncated = false; + options.terminal.onData((data) => { + options.onOutput(); + const bytes = Buffer.byteLength(data); + if (outputBytes + bytes > options.scenario.limits.output_bytes) { + truncated = true; + return; + } + outputBytes += bytes; + const atMs = normalizeProcessElapsedTime( + Date.now() - options.started, + options.scenario.normalization.time_bucket_ms, + ); + const normalized = normalizeProcessText( + data, + options.scenario, + options.temporaryRoot, + options.terminal.pid, + ); + const sequence = options.frames.length; + options.frames.push({ + sequence, + at_ms: atMs, + data: normalized, + }); + options.renderer.write(data, atMs); + options.recordEvent("frames", sequence); + }); + return () => truncated; +}; + +export const resolveProcessResult = ( + capture: PendingProcessCapture | ProcessCapture | undefined, + executionFailure: unknown, + cleanup: ProcessCaptureCleanupReport, + observations?: IncompleteProcessCaptureObservations, + partialContext?: { + readonly scenario: ProcessScenario; + readonly rootPid?: number; + }, +): ProcessCapture => { + const cleanupFailure = cleanupReportFailure(cleanup); + if (cleanupFailure !== undefined) { + const cleanupResources = Object.entries(cleanup) + .filter( + ([, outcome]) => + outcome.state === "failed" || outcome.state === "unverified", + ) + .map(([resource]) => resource); + const executionFailureReason = + describeProcessCaptureExecutionFailure(executionFailure); + let partialObservation: PartialProcessCaptureObservation | undefined; + if (capture !== undefined || observations !== undefined) { + const normalizedObservations = + observations === undefined || partialContext === undefined + ? observations + : normalizePartialProcessObservations(observations, partialContext); + const partialPayload = + capture === undefined + ? { observations: normalizedObservations } + : { capture }; + const pendingCapture = + capture === undefined + ? undefined + : { + ...capture, + settlement: { + ...capture.settlement, + cleanup_outcome: "failed" as const, + }, + }; + const partialResult = partialProcessCaptureObservationSchema.safeParse({ + ...partialPayload, + ...(pendingCapture === undefined + ? {} + : { + capture: Object.fromEntries( + Object.entries(pendingCapture).filter( + ([key]) => key !== "cleanup", + ), + ), + }), + cleanup, + execution_failure: executionFailureReason ?? null, + }); + if (partialResult.success) partialObservation = partialResult.data; + } + throw new ProcessCaptureError(cleanupFailure, { + cause: executionFailure, + reason: "cleanup_incomplete", + cleanupResources, + ...(executionFailureReason === undefined + ? {} + : { executionFailure: executionFailureReason }), + ...(partialObservation === undefined ? {} : { partialObservation }), + cleanupReport: cleanup, + }); + } + if (executionFailure instanceof ProcessCaptureError) throw executionFailure; + if (executionFailure !== undefined) { + const executionFailureReason = + describeProcessCaptureExecutionFailure(executionFailure); + throw new ProcessCaptureError("process capture failed", { + cause: executionFailure, + ...(executionFailureReason === undefined + ? {} + : { executionFailure: executionFailureReason }), + }); + } + if (capture === undefined) + throw new ProcessCaptureError("process capture produced no result"); + const unverifiedProcesses = cleanup.owned_process_group.unverified_processes; + if ("cleanup" in capture && unverifiedProcesses === undefined) return capture; + + const settlement: UnverifiedProcessCapture["settlement"] = + capture.settlement.state === "quiesced" + ? { ...capture.settlement, cleanup_outcome: "not_required" } + : { ...capture.settlement, cleanup_outcome: "cleaned" }; + try { + return parseProcessCapture({ + ...capture, + settlement, + residual_unknowns: [ + ...capture.residual_unknowns, + ...(unverifiedProcesses ?? []).map(({ pid, reason }) => ({ + scope: "process" as const, + reason: `Ownership of unrelated process ${String(pid)} could not be verified; it was left untouched: ${reason}`, + })), + ], + cleanup: { + ...("cleanup" in capture ? capture.cleanup : {}), + owned_process_group: "verified", + temporary_root: "removed", + ...(unverifiedProcesses === undefined + ? {} + : { unverified_processes: unverifiedProcesses }), + }, + }); + } catch (cause: unknown) { + throw new ProcessCaptureError( + `process capture validation failed${cause instanceof Error ? `: ${cause.message}` : ""}`, + { cause }, + ); + } +}; + +export const prepareProcessCapture = async ( + scenario: ProcessScenario, + signal: AbortSignal | undefined, + captureSnapshot: typeof snapshotRoots = snapshotRoots, + host: ProcessPreparationHost = systemProcessPreparationHost, +): Promise<{ + readonly temporaryRoot: string; + readonly runId: string; + readonly ownershipBaseline: ProcessOwnershipBaseline; + readonly before: SnapshotResult; +}> => { + assertNotCancelled(signal); + await host.prepareOwnershipInspector?.(signal); + assertNotCancelled(signal); + const before = await captureSnapshot(scenario, signal); + const temporaryRoot = await host.createTemporaryRoot(); + try { + assertNotCancelled(signal); + const runId = randomUUID(); + const ownershipBaseline = + (await host.captureOwnershipBaseline?.(signal)) ?? []; + assertNotCancelled(signal); + return { temporaryRoot, runId, ownershipBaseline, before }; + } catch (cause: unknown) { + await host.cleanup(temporaryRoot); + throw cause; + } +}; + +/** Filesystem seam for allocating and cleaning a capture's temporary root. */ +export interface ProcessPreparationHost { + prepareOwnershipInspector?(signal?: AbortSignal): Promise; + /** Capture the process baseline with the caller's startup cancellation. */ + captureOwnershipBaseline?( + signal?: AbortSignal, + ): Promise; + createTemporaryRoot(): Promise; + cleanup(path: string): Promise; +} + +const systemProcessPreparationHost: ProcessPreparationHost = { + prepareOwnershipInspector: prepareProcessOwnershipInspection, + captureOwnershipBaseline: (signal) => + systemProcessOwnershipHost.captureBaseline?.(signal) ?? Promise.resolve([]), + createTemporaryRoot: () => mkdtemp(join(tmpdir(), "rea-process-")), + cleanup: (path) => rm(path, { recursive: true, force: true }), +}; diff --git a/src/application/ProcessCaptureObservation.test.ts b/src/process/capture/ProcessCaptureObservation.test.ts similarity index 70% rename from src/application/ProcessCaptureObservation.test.ts rename to src/process/capture/ProcessCaptureObservation.test.ts index 119b153cca..9f476bf262 100644 --- a/src/application/ProcessCaptureObservation.test.ts +++ b/src/process/capture/ProcessCaptureObservation.test.ts @@ -2,16 +2,13 @@ import { expect, it } from "vitest"; import { snapshotRoots } from "./FilesystemSnapshot.js"; import { buildCaptureResult } from "./ProcessCaptureLifecycle.js"; -import { normalizeProcessSamples } from "./ProcessNormalization.js"; import { isInitializedPtyRoot, readLinuxChildren } from "./ProcessSampling.js"; import { TerminalRenderer } from "./TerminalRenderer.js"; import { - compareProcessCaptures, - parseProcessCapture, parseProcessScenario, type ProcessCapture, -} from "../domain/processCapture.js"; -import { emptyProcessCapture as emptyCapture } from "../domain/processCapture.fixture.js"; +} from "../../domain/process/processCapture.js"; +import { emptyProcessCapture as emptyCapture } from "../../domain/process/processCapture.fixture.js"; const base = { executable: "/bin/sh", @@ -139,33 +136,6 @@ it("marks redacted scripted input as an interaction unknown", () => { }); }); -it("normalizes every sampled process identifier in command text", () => { - const samples = normalizeProcessSamples( - [ - { - at_ms: 0, - pid: 101, - parent_pid: 0, - process_group_id: 101, - session_id: 101, - command: "root 101", - }, - { - at_ms: 10, - pid: 202, - parent_pid: 101, - process_group_id: 101, - session_id: 101, - command: "child 202 peer=101 unrelated 1202", - }, - ], - parseProcessScenario(base), - 101, - ); - - expect(samples[1]?.command).toBe("child peer= unrelated 1202"); -}); - it("collects and deduplicates children from every Linux thread", async () => { const signal = new AbortController().signal; expect( @@ -225,35 +195,6 @@ it("admits PTY samples only after stable session and token setup", () => { ).toBe(true); }); -it("keeps interaction and process residual uncertainty in separate scopes", () => { - const baseCapture = emptyCapture(); - const interaction = parseProcessCapture({ - ...baseCapture, - residual_unknowns: [ - { scope: "interaction", reason: "Interaction capture was partial." }, - ], - }); - const process = parseProcessCapture({ - ...baseCapture, - residual_unknowns: [ - { scope: "process", reason: "Process sampling was partial." }, - ], - }); - - expect(compareProcessCaptures(interaction, baseCapture)).toMatchObject({ - status: "unknown", - terminal: "unchanged", - interaction: "unknown", - process: "unchanged", - }); - expect(compareProcessCaptures(process, baseCapture)).toMatchObject({ - status: "unknown", - terminal: "unchanged", - interaction: "unchanged", - process: "unknown", - }); -}); - it("cancels filesystem snapshots before traversing declared roots", async () => { const controller = new AbortController(); controller.abort(); @@ -267,28 +208,3 @@ it("cancels filesystem snapshots before traversing declared roots", async () => ), ).rejects.toMatchObject({ name: "AbortError" }); }); - -it("parses bounded scenarios and rejects unordered events", () => { - const timeoutMs = parseProcessScenario(base).timeout_ms; - expect(() => - parseProcessScenario({ - ...base, - events: [ - { type: "input", at_ms: 2, data: "a" }, - { type: "input", at_ms: 1, data: "b" }, - ], - }), - ).toThrow(/ordered/); - expect( - parseProcessScenario({ - ...base, - environment: { HOME: "/caller-selected-home" }, - }).environment.HOME, - ).toBe("/caller-selected-home"); - expect(() => - parseProcessScenario({ - ...base, - events: [{ type: "input", at_ms: timeoutMs + 1, data: "late" }], - }), - ).toThrow(/after the scenario timeout/); -}); diff --git a/src/application/ProcessFilesystemEffects.ts b/src/process/capture/ProcessFilesystemEffects.ts similarity index 96% rename from src/application/ProcessFilesystemEffects.ts rename to src/process/capture/ProcessFilesystemEffects.ts index a4c7112c1b..59befa226a 100644 --- a/src/application/ProcessFilesystemEffects.ts +++ b/src/process/capture/ProcessFilesystemEffects.ts @@ -1,7 +1,7 @@ import type { FileState, FilesystemCheckpoint, -} from "../domain/processCapture.js"; +} from "../../domain/process/processCapture.js"; /** Classify path-stable filesystem effects between two bounded states. */ export const classifyFilesystemEffects = ( diff --git a/src/process/capture/ProcessHarness.test.ts b/src/process/capture/ProcessHarness.test.ts new file mode 100644 index 0000000000..961276809a --- /dev/null +++ b/src/process/capture/ProcessHarness.test.ts @@ -0,0 +1,418 @@ +import { expect, it, vi } from "vitest"; +import { processScenarioSchema } from "../../domain/process/processScenario.js"; +import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; +import { + captureProcessScenario, + normalizeCaptureFailure, + ProcessCaptureError, +} from "./ProcessHarness.js"; +import { settleProcessCaptureJournal } from "./ProcessCaptureLifecycle.js"; +import { + parseProcessCapture, + partialProcessCaptureObservationSchema, + processCaptureSchema, +} from "../../domain/process/processCapture.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../domain/process/processCapture.fixture.js"; +import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; +import { emptyProcessCapture } from "../../domain/process/processCapture.fixture.js"; +import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js"; +import { + releaseProcessResources, + resolveProcessResult, + type ProcessCaptureCleanupHost, +} from "./ProcessCaptureLifecycle.js"; +import { DarwinProcessOwnershipInspectionError } from "../DarwinProcessRunTokenReader.js"; + +it("rejects legacy replay output instead of silently discarding it", () => { + expect( + processCaptureSchema.safeParse({ + ...EMPTY_PROCESS_CAPTURE_EXAMPLE, + protocol_events: [], + }).success, + ).toBe(false); +}); + +it("preserves the actionable Darwin ownership compiler prerequisite", () => { + const normalized = normalizeCaptureFailure( + new DarwinProcessOwnershipInspectionError( + "macOS process ownership inspection requires the Apple Swift compiler via xcrun", + ), + undefined, + ); + + expect(normalized).toBeInstanceOf(ProcessCaptureError); + expect(normalized).toMatchObject({ + message: + "macOS process ownership inspection requires the Apple Swift compiler via xcrun", + }); +}); + +it("waits for terminal observations delivered after the exit callback", async () => { + const journal: Array<{ + capture_order: number; + collection: "frames"; + index: number; + }> = [{ capture_order: 0, collection: "frames", index: 0 }]; + const lateFrame = new Promise((resolve) => { + setTimeout(() => { + journal.push({ capture_order: 1, collection: "frames", index: 1 }); + resolve(); + }, 12); + }); + + let settled = false; + const wait = settleProcessCaptureJournal(journal, 20, 200).then(() => { + settled = true; + }); + await lateFrame; + expect(settled).toBe(false); + await wait; + + expect(journal).toHaveLength(2); + expect(journal[1]).toMatchObject({ collection: "frames", index: 1 }); +}); + +it("keeps empty cleanup exception messages actionable in the report", async () => { + const host: ProcessCaptureCleanupHost = { + platform: process.platform, + cleanupProcessGroup: async () => ({ + cleaned: false, + reason: "unused process cleanup", + }), + verifyTokenOwnedProcesses: async () => ({ + cleaned: false, + reason: "unused process cleanup", + }), + removeTemporaryRoot: async () => { + throw new Error(""); + }, + }; + const report = await releaseProcessResources({ + timers: new Set(), + terminal: undefined, + renderer: undefined, + runId: "fixture-run", + temporaryRoot: "/fixture/root", + host, + }); + + expect(report.temporary_root).toEqual({ state: "failed", reason: "Error" }); +}); + +it("waits for token-owned processes to exit after one cleanup signal", async () => { + vi.useFakeTimers(); + try { + let verificationCalls = 0; + const exitsAt = Date.now() + 50; + let cleanupCalls = 0; + const host: ProcessCaptureCleanupHost = { + platform: "linux", + cleanupProcessGroup: async () => { + cleanupCalls += 1; + return { cleaned: true, signaled: true }; + }, + verifyTokenOwnedProcesses: async () => { + verificationCalls += 1; + return Date.now() < exitsAt + ? { cleaned: false, reason: "owned process is exiting" } + : { cleaned: true, signaled: false }; + }, + removeTemporaryRoot: async () => undefined, + }; + const result = releaseProcessResources({ + timers: new Set(), + terminal: { pid: 321 }, + renderer: undefined, + runId: "fixture-run", + temporaryRoot: "/fixture/root", + host, + }); + + await vi.runAllTimersAsync(); + const report = await result; + + expect(report.owned_process_group).toEqual({ + state: "cleaned", + reason: null, + }); + expect(cleanupCalls).toBe(1); + expect(verificationCalls).toBeGreaterThan(1); + } finally { + vi.useRealTimers(); + } +}); + +it("retains unrelated withheld processes through cleanup, capture, and JSON", async () => { + const host: ProcessCaptureCleanupHost = { + platform: "darwin", + cleanupProcessGroup: async () => ({ + cleaned: true, + signaled: true, + unverified: [ + { pid: 900, diagnostic: "platform_binary_environment_withheld" }, + ], + }), + verifyTokenOwnedProcesses: async () => ({ + cleaned: true, + signaled: false, + unverified: [{ pid: 901, diagnostic: "foreign uid: EINVAL" }], + }), + removeTemporaryRoot: async () => undefined, + }; + const report = await releaseProcessResources({ + timers: new Set(), + terminal: { pid: 321 }, + renderer: undefined, + runId: "fixture-run", + temporaryRoot: "/fixture/root", + host, + }); + const unverified = [ + { pid: 900, reason: "platform_binary_environment_withheld" }, + { pid: 901, reason: "foreign uid: EINVAL" }, + ]; + expect(report.owned_process_group).toEqual({ + state: "cleaned", + reason: null, + unverified_processes: unverified, + }); + const original = emptyProcessCapture(); + const { cleanup: previousCleanup, ...pending } = original; + for (const input of [pending, original]) { + const resolved = resolveProcessResult(input, undefined, report); + const serialized: unknown = JSON.parse(JSON.stringify(resolved)); + const capture = parseProcessCapture(serialized); + expect(capture.cleanup).toEqual({ + ...previousCleanup, + unverified_processes: unverified, + }); + expect(capture.residual_unknowns).toEqual([ + ...original.residual_unknowns, + { + scope: "process", + reason: expect.stringContaining("900 could not be verified"), + }, + { + scope: "process", + reason: expect.stringContaining("901 could not be verified"), + }, + ]); + } +}); + +it("retains the last ownership failure when the verification grace expires", async () => { + vi.useFakeTimers(); + try { + let verificationCalls = 0; + let cleanupCalls = 0; + const host: ProcessCaptureCleanupHost = { + platform: "linux", + cleanupProcessGroup: async () => { + cleanupCalls += 1; + return { cleaned: true, signaled: true }; + }, + verifyTokenOwnedProcesses: async () => { + verificationCalls += 1; + return { + cleaned: false, + reason: + verificationCalls === 1 + ? "owned process has not exited yet" + : "owned process remained after identity recheck", + }; + }, + removeTemporaryRoot: async () => undefined, + }; + const result = releaseProcessResources({ + timers: new Set(), + terminal: { pid: 322 }, + renderer: undefined, + runId: "fixture-run", + temporaryRoot: "/fixture/root", + host, + }); + + await vi.runAllTimersAsync(); + const report = await result; + + expect(report.owned_process_group).toEqual({ + state: "unverified", + reason: "owned process remained after identity recheck", + }); + expect(cleanupCalls).toBe(1); + expect(verificationCalls).toBeGreaterThan(1); + } finally { + vi.useRealTimers(); + } +}); + +it("passes sampled detached groups into cleanup even when their token is unknown", async () => { + let sampledGroups: readonly number[] | undefined; + const host: ProcessCaptureCleanupHost = { + platform: "linux", + cleanupProcessGroup: async (ownership) => { + sampledGroups = ownership.sampledProcessGroupIds; + return { + cleaned: false, + reason: "sampled group has no verifiable run token", + }; + }, + verifyTokenOwnedProcesses: async () => ({ + cleaned: true, + signaled: false, + }), + removeTemporaryRoot: async () => undefined, + }; + + const report = await releaseProcessResources({ + timers: new Set(), + terminal: { pid: 321 }, + renderer: undefined, + runId: "fixture-run", + temporaryRoot: "/fixture/root", + sampledProcessGroupIds: [654], + host, + }); + + expect(sampledGroups).toEqual([654]); + expect(report.owned_process_group).toEqual({ + state: "unverified", + reason: "sampled group has no verifiable run token", + }); +}); + +it("retains observations and both causes when process cleanup is unverifiable", () => { + const verified = emptyProcessCapture(); + const capture = parseProcessCapture({ + ...verified, + frames: [{ sequence: 0, at_ms: 0, data: "observed output" }], + process_samples: [ + { + at_ms: 1, + pid: 654, + parent_pid: 321, + command: "sanitized-detached-child", + process_group_id: 654, + session_id: 654, + }, + ], + event_journal: [], + }); + const executionFailure = new Error("capture ended after a fixture error"); + const cleanup = { + owned_process_group: { + state: "unverified" as const, + reason: + "process ownership token could not be read for 1 live process(es): environment_unavailable=1", + unverified_processes: [ + { pid: 900, reason: "platform_binary_environment_withheld" }, + ], + }, + terminal_renderer: { state: "cleaned" as const, reason: null }, + temporary_root: { state: "cleaned" as const, reason: null }, + }; + + let error: ProcessCaptureError | undefined; + try { + resolveProcessResult(capture, executionFailure, cleanup); + } catch (cause: unknown) { + if (!(cause instanceof ProcessCaptureError)) throw cause; + error = cause; + } + expect(error).toBeDefined(); + if (error === undefined) throw new Error("expected cleanup-incomplete error"); + + const projection = projectAnalysisError(error); + const parsedProjection = analysisErrorProjectionSchema.parse(projection); + expect(parsedProjection).toMatchObject({ + code: "cleanup_incomplete", + details: { + cleanup_report: cleanup, + execution_failure: "capture ended after a fixture error", + partial_observation: { + capture: { + frames: [{ data: "observed output" }], + process_samples: [{ pid: 654, process_group_id: 654 }], + settlement: { cleanup_outcome: "failed" }, + }, + execution_failure: "capture ended after a fixture error", + }, + }, + }); + expect(error.cause).toBe(executionFailure); + const partialObservation = error.partialObservation; + expect(partialObservation).toBeDefined(); + if (partialObservation === undefined) + throw new Error("expected validated partial observation"); + expect( + partialProcessCaptureObservationSchema.safeParse({ + ...partialObservation, + cleanup: { + owned_process_group: { state: "cleaned", reason: null }, + terminal_renderer: { state: "cleaned", reason: null }, + temporary_root: { state: "cleaned", reason: null }, + }, + }).success, + ).toBe(false); + if (!("capture" in partialObservation)) + throw new Error("expected completed partial capture observations"); + const partialCapture = partialObservation.capture; + expect(() => parseProcessCapture(partialCapture)).toThrow(); +}); + +it("projects execution and cleanup failures when capture never completed", () => { + const executionFailure = new Error("terminal startup failed"); + const cleanup = { + owned_process_group: { + state: "unverified" as const, + reason: "process ownership token could not be read", + }, + terminal_renderer: { state: "cleaned" as const, reason: null }, + temporary_root: { state: "cleaned" as const, reason: null }, + }; + + let error: ProcessCaptureError | undefined; + try { + resolveProcessResult(undefined, executionFailure, cleanup); + } catch (cause: unknown) { + if (!(cause instanceof ProcessCaptureError)) throw cause; + error = cause; + } + expect(error).toBeDefined(); + if (error === undefined) throw new Error("expected cleanup-incomplete error"); + + expect(error.partialObservation).toBeUndefined(); + expect(error.cause).toBe(executionFailure); + expect(projectAnalysisError(error)).toMatchObject({ + code: "cleanup_incomplete", + details: { + cleanup_report: cleanup, + execution_failure: "terminal startup failed", + }, + }); +}); + +it("fails closed on Windows before resolving or launching scenario paths", async () => { + const scenario = processScenarioSchema.parse({ + executable: "Z:/missing/should-never-be-resolved.exe", + working_directory: "Z:/missing/working-directory", + }); + const result = await captureProcessScenario(scenario, undefined, "win32"); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected Windows ownership refusal"); + if (!(result.error instanceof AnalysisCapabilityUnavailableError)) + throw new Error("expected a capability-unavailable outcome"); + expect(result.error).toMatchObject({ + operation: "capture_process_scenario", + reason: expect.stringContaining("Windows PTY process capture"), + }); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "capability_unavailable", + category: "unsupported_provider", + message: expect.stringContaining("does not yet verify descendant cleanup"), + details: { + operation: "capture_process_scenario", + reason: result.error.reason, + }, + }); +}); diff --git a/src/application/ProcessHarness.ts b/src/process/capture/ProcessHarness.ts similarity index 53% rename from src/application/ProcessHarness.ts rename to src/process/capture/ProcessHarness.ts index 822f8f9898..3d7af9c803 100644 --- a/src/application/ProcessHarness.ts +++ b/src/process/capture/ProcessHarness.ts @@ -8,12 +8,13 @@ import type { ProcessScenario, RecordProcessCaptureEvent, TerminalFrame, -} from "../domain/processCapture.js"; -import { parseProcessCapture } from "../domain/processCapture.js"; -import { err, ok, type Result } from "../domain/result.js"; -import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; -import { type AnalysisError } from "../domain/analysisErrorBase.js"; +} from "../../domain/process/processCapture.js"; +import { parseProcessCapture } from "../../domain/process/processCapture.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; +import { type AnalysisError } from "../../domain/analysisErrorBase.js"; import { + describeProcessCaptureExecutionFailure, ProcessCaptureError, processCaptureCancelled, } from "./ProcessCaptureError.js"; @@ -23,17 +24,23 @@ import { snapshotRoots } from "./FilesystemSnapshot.js"; import { TerminalRenderer } from "./TerminalRenderer.js"; import type { ProcessTimer } from "./ProcessTimer.js"; import { normalizeProcessText } from "./ProcessNormalization.js"; -import { selectCapturedProcessGroupIds } from "../process/ProcessOwnershipProcessTree.js"; +import { selectCapturedProcessGroupIds } from "../ProcessOwnershipProcessTree.js"; import { awaitTerminalExit, buildCaptureResult, + cleanupReportFailure, captureTerminalFrames, + createProcessCaptureObservationBuffer, createRunManifest, + observeLaunchedExecutable, + observeSelectedExecutable, observeSettlement, prepareProcessCapture, releaseProcessResources, resolveProcessResult, settleProcessCaptureJournal, + type ProcessCaptureCleanupHost, + type ProcessCaptureObservationBuffer, type PendingProcessCapture, } from "./ProcessCaptureLifecycle.js"; import { @@ -47,6 +54,8 @@ import { import { makeProcessCaptureEnvironment } from "./ProcessCaptureEnvironment.js"; import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js"; import { processCaptureOwnershipUnavailableReason } from "./ProcessCaptureCapability.js"; +import type { ProcessOwnershipBaseline } from "../ProcessOwnership.js"; +import { DarwinProcessOwnershipInspectionError } from "../DarwinProcessRunTokenReader.js"; export { probeProcessCaptureCapability } from "./ProcessCaptureCapability.js"; interface StartedCaptureRuntime { @@ -54,6 +63,7 @@ interface StartedCaptureRuntime { readonly terminal: IPty; readonly started: number; readonly startedAt: Date; + readonly executableIdentity: ReturnType; readonly lastOutput: () => number; readonly framesTruncated: () => boolean; readonly stopSampler: () => Promise<{ readonly partial: boolean }>; @@ -65,19 +75,58 @@ const cleanupFailedStartup = async (options: { readonly terminal: IPty | undefined; readonly renderer: TerminalRenderer | undefined; readonly runId: string; + readonly captureBaseline: ProcessOwnershipBaseline; + readonly scenario: ProcessScenario; + readonly cleanupHost?: ProcessCaptureCleanupHost; + readonly observations?: ProcessCaptureObservationBuffer; readonly temporaryRoot: string; }): Promise => { + const sampledProcessGroupIds = + options.terminal !== undefined && + options.observations?.process_samples.state === "available" + ? selectCapturedProcessGroupIds( + options.terminal.pid, + options.observations.process_samples.value, + ).filter((groupId) => groupId !== options.terminal?.pid) + : undefined; options.terminal?.kill("SIGKILL"); + if (options.observations !== undefined && options.renderer !== undefined) { + try { + options.observations.rendered_frames = { + state: "available", + value: await options.renderer.frames(), + }; + } catch (cause: unknown) { + options.observations.rendered_frames = { + state: "unavailable", + reason: `Rendered terminal frame collection failed: ${cause instanceof Error ? cause.message : "unknown failure"}`, + }; + } + } const cleanupFailure = await releaseProcessResources({ - ...options, - capturedProcessGroupIds: - options.terminal === undefined ? [] : [options.terminal.pid], + timers: options.timers, + terminal: options.terminal, + renderer: options.renderer, + runId: options.runId, + captureBaseline: options.captureBaseline, + ...(sampledProcessGroupIds === undefined ? {} : { sampledProcessGroupIds }), + temporaryRoot: options.temporaryRoot, + ...(options.cleanupHost === undefined ? {} : { host: options.cleanupHost }), }); - if (cleanupFailure !== undefined) - throw new ProcessCaptureError(cleanupFailure, { - cause: options.cause, - reason: "cleanup_incomplete", - }); + if (cleanupReportFailure(cleanupFailure) !== undefined) { + resolveProcessResult( + undefined, + options.cause, + cleanupFailure, + options.observations, + { + scenario: options.scenario, + ...(options.terminal === undefined + ? {} + : { rootPid: options.terminal.pid }), + }, + ); + } throw options.cause; }; @@ -102,6 +151,10 @@ interface StartCaptureRuntimeOptions { readonly hostEnvironment: Readonly>; readonly temporaryRoot: string; readonly runId: string; + readonly ownershipBaseline: ProcessOwnershipBaseline; + readonly cleanupHost?: ProcessCaptureCleanupHost; + readonly observationBuffer: ProcessCaptureObservationBuffer; + readonly onSpawn: (pid: number) => void; readonly frames: TerminalFrame[]; readonly samples: ProcessSample[]; readonly interactions: InteractionEvent[]; @@ -119,6 +172,10 @@ const startCaptureRuntime = async ( let terminal: IPty | undefined; try { const { spawn } = await import("@lydell/node-pty"); + const selectedExecutable = await observeSelectedExecutable( + scenario.executable, + options.signal, + ); const startedAt = new Date(); const started = Date.now(); let lastOutput = started; @@ -135,6 +192,15 @@ const startCaptureRuntime = async ( rows: scenario.terminal.rows, name: "xterm-256color", }); + options.onSpawn(terminal.pid); + options.observationBuffer.target_pid = { + state: "available", + value: scenario.normalization.pids ? 1 : terminal.pid, + }; + const executableIdentity = observeLaunchedExecutable( + scenario.executable, + selectedExecutable, + ); const framesTruncated = captureTerminalFrames({ ...options, terminal, @@ -161,6 +227,7 @@ const startCaptureRuntime = async ( terminal, started, startedAt, + executableIdentity, lastOutput: () => lastOutput, framesTruncated, stopSampler, @@ -172,6 +239,12 @@ const startCaptureRuntime = async ( terminal, renderer, runId: options.runId, + captureBaseline: options.ownershipBaseline, + scenario, + ...(options.cleanupHost === undefined + ? {} + : { cleanupHost: options.cleanupHost }), + observations: options.observationBuffer, temporaryRoot: options.temporaryRoot, }); } @@ -181,27 +254,36 @@ const finishProcessRun = async (options: { readonly runtime: StartedCaptureRuntime | undefined; readonly timers: Set; readonly runId: string; + readonly captureBaseline: ProcessOwnershipBaseline; + readonly cleanupHost?: ProcessCaptureCleanupHost; readonly temporaryRoot: string; + readonly scenario: ProcessScenario; readonly samples: readonly ProcessSample[]; readonly stopSampler: () => Promise<{ readonly partial: boolean }>; readonly capture: PendingProcessCapture | undefined; readonly executionFailure: unknown; + readonly observations?: ProcessCaptureObservationBuffer; + readonly rootPid?: number; }): Promise => { await options.stopSampler(); - const cleanupFailure = await releaseProcessResources({ + const sampledProcessGroupIds = + options.runtime === undefined + ? undefined + : selectCapturedProcessGroupIds( + options.runtime.terminal.pid, + options.samples, + ).filter((groupId) => groupId !== options.runtime?.terminal.pid); + const cleanup = await releaseProcessResources({ timers: options.timers, terminal: options.runtime?.terminal, renderer: options.runtime?.renderer, runId: options.runId, + captureBaseline: options.captureBaseline, + ...(sampledProcessGroupIds === undefined ? {} : { sampledProcessGroupIds }), temporaryRoot: options.temporaryRoot, - capturedProcessGroupIds: - options.runtime === undefined - ? [] - : selectCapturedProcessGroupIds( - options.runtime.terminal.pid, - options.samples, - ), + ...(options.cleanupHost === undefined ? {} : { host: options.cleanupHost }), }); + const cleanupFailure = cleanupReportFailure(cleanup); let { capture, executionFailure } = options; let verifiedCapture: ProcessCapture | undefined; if (capture !== undefined && cleanupFailure === undefined) { @@ -226,13 +308,37 @@ const finishProcessRun = async (options: { ); } } + const rootPid = options.runtime?.terminal.pid ?? options.rootPid; + const partialContext = + rootPid === undefined + ? { scenario: options.scenario } + : { scenario: options.scenario, rootPid }; return resolveProcessResult( - verifiedCapture, + cleanupFailure === undefined ? verifiedCapture : capture, executionFailure, - cleanupFailure, + cleanup, + options.observations, + partialContext, ); }; +/** Preserve typed process-inspection preflight failures at the capture boundary. */ +export const normalizeCaptureFailure = ( + cause: unknown, + signal: AbortSignal | undefined, +): unknown => { + if (cause instanceof ProcessCaptureError) return cause; + if (cause instanceof DarwinProcessOwnershipInspectionError) + return new ProcessCaptureError(cause.message, { cause }); + if ( + signal?.aborted === true && + (cause === signal.reason || + (cause instanceof Error && cause.name === "AbortError")) + ) + return processCaptureCancelled(); + return cause; +}; + const completeCapture = async (options: { readonly scenario: ProcessScenario; readonly hostPlatform: NodeJS.Platform; @@ -245,26 +351,75 @@ const completeCapture = async (options: { readonly interactions: readonly InteractionEvent[]; readonly exit: Awaited>; readonly signal?: AbortSignal; + readonly captureSnapshot: typeof snapshotRoots; readonly initiallyTruncated: boolean; readonly eventJournal: readonly ProcessCaptureEventJournalEntry[]; + readonly observationBuffer: ProcessCaptureObservationBuffer; readonly recordEvent: RecordProcessCaptureEvent; }): Promise => { + const unavailableReason = (stage: string, cause: unknown): string => + `${stage} failed: ${cause instanceof Error ? cause.message : "unknown failure"}`; const { runtime, scenario } = options; const { reason } = options.exit; if (reason === "cancelled") throw processCaptureCancelled(); - const settlement = await observeSettlement( - options.runId, - selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), - scenario.settle_ms, - options.recordEvent, - options.hostPlatform, - ); + let settlement: Awaited>; + try { + settlement = await observeSettlement( + options.runId, + selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), + scenario.settle_ms, + options.recordEvent, + options.hostPlatform, + ); + } catch (cause: unknown) { + options.observationBuffer.settlement = { + state: "unavailable", + reason: unavailableReason("Process settlement observation", cause), + }; + throw cause; + } + options.observationBuffer.settlement = { + state: "available", + value: settlement, + }; const samplingPartial = (await runtime.stopSampler()).partial; await settleProcessCaptureJournal(options.eventJournal); assertNotCancelled(options.signal); - const after = await snapshotRoots(scenario, options.signal); + let after: Awaited>; + try { + after = await options.captureSnapshot(scenario, options.signal); + } catch (cause: unknown) { + options.observationBuffer.filesystem_snapshots = { + ...options.observationBuffer.filesystem_snapshots, + after: { + state: "unavailable", + reason: unavailableReason("Final filesystem snapshot", cause), + }, + }; + throw cause; + } + options.observationBuffer.filesystem_snapshots = { + ...options.observationBuffer.filesystem_snapshots, + after: { + state: "available", + value: { files: after.files, truncated: after.truncated }, + }, + }; options.recordEvent("filesystem_checkpoints", 1); - const renderedFrames = await runtime.renderer.frames(); + let renderedFrames: Awaited>; + try { + renderedFrames = await runtime.renderer.frames(); + } catch (cause: unknown) { + options.observationBuffer.rendered_frames = { + state: "unavailable", + reason: unavailableReason("Rendered terminal frame collection", cause), + }; + throw cause; + } + options.observationBuffer.rendered_frames = { + state: "available", + value: renderedFrames, + }; const checkpoints: UnverifiedProcessCapture["filesystem_checkpoints"] = [ { name: "before", @@ -281,12 +436,23 @@ const completeCapture = async (options: { truncated: after.truncated, }, ]; - const manifest = await createRunManifest( - scenario, - runtime.startedAt, - new Date(), - { platform: options.hostPlatform, architecture: process.arch }, - ); + let manifest: Awaited>; + try { + manifest = await createRunManifest( + scenario, + runtime.startedAt, + new Date(), + runtime.executableIdentity, + { platform: options.hostPlatform, architecture: process.arch }, + ); + } catch (cause: unknown) { + options.observationBuffer.manifest = { + state: "unavailable", + reason: unavailableReason("Capture manifest creation", cause), + }; + throw cause; + } + options.observationBuffer.manifest = { state: "available", value: manifest }; const truncated = options.initiallyTruncated || after.truncated || @@ -319,22 +485,30 @@ const runProcessScenario = async ( signal?: AbortSignal, hostEnvironment: Readonly> = process.env, hostPlatform: NodeJS.Platform = process.platform, + captureSnapshot: typeof snapshotRoots = snapshotRoots, + cleanupHost?: ProcessCaptureCleanupHost, ): Promise => { - const { temporaryRoot, runId, before } = await prepareProcessCapture( - scenario, - signal, - ); + const { temporaryRoot, runId, ownershipBaseline, before } = + await prepareProcessCapture(scenario, signal, captureSnapshot); const frames: TerminalFrame[] = []; const samples: ProcessSample[] = []; + const interactions: InteractionEvent[] = []; const journal = createProcessCaptureJournal(); const { entries: eventJournal, recordEvent } = journal; recordEvent("filesystem_checkpoints", 0); + const observations = createProcessCaptureObservationBuffer({ + frames, + interactions, + samples, + eventJournal, + before, + }); let runtime: StartedCaptureRuntime | undefined; + let actualRootPid: number | undefined; const timers = new Set(); let capture: PendingProcessCapture | undefined; let executionFailure: unknown; let stopSampler = async () => ({ partial: false }); - const interactions: InteractionEvent[] = []; const dispatchedEventIndexes = new Set(); try { @@ -343,6 +517,12 @@ const runProcessScenario = async ( hostEnvironment, temporaryRoot, runId, + ownershipBaseline, + ...(cleanupHost === undefined ? {} : { cleanupHost }), + observationBuffer: observations, + onSpawn: (pid) => { + actualRootPid = pid; + }, frames, samples, interactions, @@ -363,6 +543,15 @@ const runProcessScenario = async ( dispatchedEventIndexes, recordEvent, }); + observations.exit = { + state: "available", + value: { + code: + exit.reason === "exited" && exit.exitCode >= 0 ? exit.exitCode : null, + signal: exit.signal ?? null, + reason: exit.reason, + }, + }; capture = await completeCapture({ scenario, hostPlatform, @@ -376,22 +565,52 @@ const runProcessScenario = async ( exit, initiallyTruncated: before.truncated, eventJournal, + observationBuffer: observations, recordEvent, + captureSnapshot, ...(signal === undefined ? {} : { signal }), }); } catch (cause: unknown) { runtime?.terminal.kill("SIGKILL"); - executionFailure = cause; + executionFailure = normalizeCaptureFailure(cause, signal); + if ( + runtime !== undefined && + observations.rendered_frames.state === "unavailable" && + observations.rendered_frames.reason.startsWith( + "Rendered terminal frames were not collected", + ) + ) { + try { + observations.rendered_frames = { + state: "available", + value: await runtime.renderer.frames(), + }; + } catch (renderCause: unknown) { + const renderFailure = + renderCause instanceof Error + ? renderCause.message + : "unknown failure"; + observations.rendered_frames = { + state: "unavailable", + reason: `Rendered terminal frame collection failed: ${renderFailure}`, + }; + } + } } return finishProcessRun({ runtime, timers, runId, + captureBaseline: ownershipBaseline, + ...(cleanupHost === undefined ? {} : { cleanupHost }), temporaryRoot, + scenario, samples, stopSampler, capture, executionFailure, + observations, + ...(actualRootPid === undefined ? {} : { rootPid: actualRootPid }), }); }; @@ -401,6 +620,8 @@ export const captureProcessScenario = async ( signal?: AbortSignal, platform: NodeJS.Platform = process.platform, environment: Readonly> = process.env, + captureSnapshot: typeof snapshotRoots = snapshotRoots, + cleanupHost?: ProcessCaptureCleanupHost, ): Promise> => { const ownershipReason = processCaptureOwnershipUnavailableReason(platform); if (ownershipReason !== undefined) @@ -419,13 +640,28 @@ export const captureProcessScenario = async ( environment, ); return ok( - await runProcessScenario(resolvedScenario, signal, environment, platform), + await runProcessScenario( + resolvedScenario, + signal, + environment, + platform, + captureSnapshot, + cleanupHost, + ), ); } catch (cause: unknown) { + const failure = normalizeCaptureFailure(cause, signal); + const executionFailureReason = + describeProcessCaptureExecutionFailure(cause); return err( - cause instanceof ProcessCaptureError - ? cause - : new ProcessCaptureError("process capture failed", { cause }), + failure instanceof ProcessCaptureError + ? failure + : new ProcessCaptureError("process capture failed", { + cause, + ...(executionFailureReason === undefined + ? {} + : { executionFailure: executionFailureReason }), + }), ); } }; diff --git a/src/process/capture/ProcessNormalization.test.ts b/src/process/capture/ProcessNormalization.test.ts new file mode 100644 index 0000000000..7a15b8b966 --- /dev/null +++ b/src/process/capture/ProcessNormalization.test.ts @@ -0,0 +1,159 @@ +import { expect, it } from "vitest"; + +import { normalizeProcessSamples } from "./ProcessNormalization.js"; +import { parseProcessScenario } from "../../domain/process/processCapture.js"; + +const rootPid = 41_010; +const samples = Object.freeze( + [ + { + at_ms: 17, + pid: rootPid, + parent_pid: 40_000, + process_group_id: rootPid, + session_id: rootPid, + command: " root 41010 ", + }, + { + at_ms: 28, + pid: 42_020, + parent_pid: rootPid, + process_group_id: 43_030, + session_id: 44_040, + command: + " /workspace/program child 42020 parent 41010 group 43030 session 44040 listen:8080 marker ", + }, + { + at_ms: 39, + pid: 45_050, + parent_pid: 42_020, + process_group_id: 43_030, + session_id: 44_040, + command: "peer 45050 unrelated 145050", + }, + { + at_ms: 41, + pid: 46_060, + parent_pid: 0, + process_group_id: null, + session_id: 0, + command: "unknown 46060", + }, + { + at_ms: 52, + pid: 47_070, + parent_pid: 0, + process_group_id: 0, + session_id: null, + command: "unavailable 47070", + }, + ].map((sample) => + Object.freeze({ ...sample, runtime_metadata: "must not be serialized" }), + ), +); + +const scenario = (normalization: { + readonly pids?: boolean; + readonly paths?: boolean; + readonly ports?: boolean; +}) => + parseProcessScenario({ + executable: "/usr/bin/node", + working_directory: "/workspace", + normalization: { + paths: true, + ...normalization, + time_bucket_ms: 10, + patterns: [{ pattern: "marker", replacement: "selected" }], + }, + }); + +it("normalizes representative process identities and preserves null, zero, and unrelated numbers", () => { + const before = JSON.stringify(samples); + const normalized = normalizeProcessSamples(samples, scenario({}), rootPid); + + expect(normalized).toEqual([ + { + at_ms: 10, + pid: 1, + parent_pid: 2, + process_group_id: 1, + session_id: 1, + command: "root ", + }, + { + at_ms: 20, + pid: 3, + parent_pid: 1, + process_group_id: 4, + session_id: 5, + command: + "/program child parent group session listen: selected", + }, + { + at_ms: 30, + pid: 6, + parent_pid: 3, + process_group_id: 4, + session_id: 5, + command: "peer unrelated 145050", + }, + { + at_ms: 40, + pid: 7, + parent_pid: 0, + process_group_id: null, + session_id: 0, + command: "unknown ", + }, + { + at_ms: 50, + pid: 8, + parent_pid: 0, + process_group_id: 0, + session_id: null, + command: "unavailable ", + }, + ]); + expect(JSON.stringify(samples)).toBe(before); + expect(normalized.every((sample) => !("runtime_metadata" in sample))).toBe( + true, + ); + + const pathAndPortPreserved = normalizeProcessSamples( + samples, + scenario({ paths: false, ports: false }), + rootPid, + ); + expect(pathAndPortPreserved[1]).toMatchObject({ + pid: 3, + parent_pid: 1, + command: + "/workspace/program child parent group session listen:8080 selected", + }); + + const identifiersPreserved = normalizeProcessSamples( + samples, + scenario({ pids: false }), + rootPid, + ); + expect( + identifiersPreserved.map( + ({ pid, parent_pid, process_group_id, session_id }) => [ + pid, + parent_pid, + process_group_id, + session_id, + ], + ), + ).toEqual([ + [rootPid, 40_000, rootPid, rootPid], + [42_020, rootPid, 43_030, 44_040], + [45_050, 42_020, 43_030, 44_040], + [46_060, 0, null, 0], + [47_070, 0, 0, null], + ]); + expect(identifiersPreserved[1]?.command).toBe( + "/program child 42020 parent 41010 group 43030 session 44040 listen: selected", + ); +}); diff --git a/src/application/ProcessNormalization.ts b/src/process/capture/ProcessNormalization.ts similarity index 98% rename from src/application/ProcessNormalization.ts rename to src/process/capture/ProcessNormalization.ts index aff05d33af..347c0e5cfb 100644 --- a/src/application/ProcessNormalization.ts +++ b/src/process/capture/ProcessNormalization.ts @@ -1,7 +1,7 @@ import type { ProcessSample, ProcessScenario, -} from "../domain/processCapture.js"; +} from "../../domain/process/processCapture.js"; /** Bucket one elapsed process-capture timestamp under scenario normalization. */ export const normalizeProcessElapsedTime = ( diff --git a/src/application/ProcessSampling.ts b/src/process/capture/ProcessSampling.ts similarity index 98% rename from src/application/ProcessSampling.ts rename to src/process/capture/ProcessSampling.ts index b559a8f7ac..e8ee7cefb4 100644 --- a/src/application/ProcessSampling.ts +++ b/src/process/capture/ProcessSampling.ts @@ -2,9 +2,9 @@ import { readFile, readdir } from "node:fs/promises"; import type { ProcessSample, RecordProcessCaptureEvent, -} from "../domain/processCapture.js"; -import { execFileOutput } from "../process/ExecFileOutput.js"; -import { readProcessRunId } from "../process/ProcessOwnership.js"; +} from "../../domain/process/processCapture.js"; +import { execFileOutput } from "../ExecFileOutput.js"; +import { readProcessRunId } from "../ProcessOwnership.js"; const PROC_READ_CONCURRENCY = 64; interface ProcessRow { diff --git a/src/application/ProcessScenarioRuntimeValidation.test.ts b/src/process/capture/ProcessScenarioRuntimeValidation.test.ts similarity index 93% rename from src/application/ProcessScenarioRuntimeValidation.test.ts rename to src/process/capture/ProcessScenarioRuntimeValidation.test.ts index 922df07eeb..5e70a226ce 100644 --- a/src/application/ProcessScenarioRuntimeValidation.test.ts +++ b/src/process/capture/ProcessScenarioRuntimeValidation.test.ts @@ -7,7 +7,7 @@ import { resolveExecutable, resolveProcessScenarioRuntimePaths, } from "./ProcessScenarioRuntimeValidation.js"; -import { parseProcessScenario } from "../domain/processCapture.js"; +import { parseProcessScenario } from "../../domain/process/processCapture.js"; it("preserves the caller-selected executable symlink for process invocation", async () => { const root = await mkdtemp(join(tmpdir(), "rea-process-executable-alias-")); diff --git a/src/application/ProcessScenarioRuntimeValidation.ts b/src/process/capture/ProcessScenarioRuntimeValidation.ts similarity index 97% rename from src/application/ProcessScenarioRuntimeValidation.ts rename to src/process/capture/ProcessScenarioRuntimeValidation.ts index 59cddcf24a..e71e16d1e5 100644 --- a/src/application/ProcessScenarioRuntimeValidation.ts +++ b/src/process/capture/ProcessScenarioRuntimeValidation.ts @@ -2,7 +2,7 @@ import { access, realpath, stat } from "node:fs/promises"; import { constants as fsConstants } from "node:fs"; import { delimiter, isAbsolute, join, resolve } from "node:path"; -import type { ProcessScenario } from "../domain/processCapture.js"; +import type { ProcessScenario } from "../../domain/process/processCapture.js"; import { ProcessCaptureError, processCaptureCancelled, diff --git a/src/application/ProcessTimer.test.ts b/src/process/capture/ProcessTimer.test.ts similarity index 100% rename from src/application/ProcessTimer.test.ts rename to src/process/capture/ProcessTimer.test.ts diff --git a/src/application/ProcessTimer.ts b/src/process/capture/ProcessTimer.ts similarity index 100% rename from src/application/ProcessTimer.ts rename to src/process/capture/ProcessTimer.ts diff --git a/src/application/TerminalRenderer.ts b/src/process/capture/TerminalRenderer.ts similarity index 98% rename from src/application/TerminalRenderer.ts rename to src/process/capture/TerminalRenderer.ts index 44bf4d5809..0a0492008d 100644 --- a/src/application/TerminalRenderer.ts +++ b/src/process/capture/TerminalRenderer.ts @@ -3,7 +3,7 @@ import { createRequire } from "node:module"; import type { RecordProcessCaptureEvent, RenderedTerminalFrame, -} from "../domain/processCapture.js"; +} from "../../domain/process/processCapture.js"; const require = createRequire(import.meta.url); // SAFETY: both pinned xterm packages publish CommonJS at runtime and matching declarations. diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 68b1aeb082..d011e5ac85 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -1,3 +1,12 @@ +import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; +import { createEvmInterfaceService } from "../composition/evm.js"; +import { registerEvmTools } from "./registerEvmTools.js"; +import { registerRecordedCrashTools } from "./registerRecordedCrashTools.js"; +import { createRecordedCrashService } from "../composition/binaryDiagnostics.js"; +import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js"; +import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js"; +import { createBinaryLayoutService } from "../composition/binaryDiagnostics.js"; +import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; import { McpServer } from "@modelcontextprotocol/server"; import { isAbsolute } from "node:path"; @@ -25,6 +34,9 @@ import type { WebSourceLocationService } from "../application/WebSourceLocationS import type { WebRuntimeService } from "../application/WebRuntimeService.js"; import { createWebRuntimeService } from "../composition/webRuntime.js"; import { registerWebRuntimeTools } from "./registerWebRuntimeTools.js"; +import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js"; +import { createWebNetworkCaptureService } from "../composition/webNetworkCaptures.js"; +import { registerWebNetworkCaptureTool } from "./registerWebNetworkCaptureTool.js"; import { registerJavaScriptRecoveryTool } from "./registerJavaScriptRecoveryTool.js"; import { JavaScriptRecoveryService } from "../application/javascript/JavaScriptRecoveryService.js"; import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js"; @@ -56,12 +68,16 @@ const ACTIVE_TARGET_INSTRUCTIONS = "REA analyzes the active reverse-engineering target. Use the analysis tool that answers the question directly. Search or list symbols when discovery is needed; analyze_function provides a function dossier, and focused procedure tools return individual facets."; export interface CreateServerOptions { + readonly evmInterface?: EvmInterfaceService; readonly logger?: Logger; + readonly binaryLayout?: BinaryLayoutService; + readonly recordedCrash?: RecordedCrashService; readonly firmwareAnalysis?: FirmwareAnalysisPort; readonly javascriptRecovery?: JavaScriptRecoveryPort; readonly webModuleTrace?: WebModuleTraceService; readonly webSourceLocation?: WebSourceLocationService; readonly webRuntime?: WebRuntimeService; + readonly webNetworkCapture?: WebNetworkCaptureService; readonly androidAnalysis?: AndroidAnalysisPort; readonly browserObservation?: BrowserObservationPort; readonly browserScenarioCapture?: BrowserScenarioCapturePort; @@ -81,9 +97,22 @@ const installSessionToolAvailability = ( const policy = sessionAvailabilityPolicy(options.availabilityPolicy, { optionalProviderLoadFailures: options.optionalProviderLoadFailures, optionalFeatures: { + evmInterfaceEnabled: + options.evmInterface !== undefined || + (process.platform === "linux" && process.arch === "x64"), webModuleResolutionEnabled: options.webModuleTrace !== undefined || isAbsolute(process.env.REA_BROWSER_EXECUTABLE ?? ""), + recordedCrashEnabled: + options.recordedCrash !== undefined || + (process.platform === "linux" && + process.arch === "x64" && + isAbsolute(process.env.REA_PWNTOOLS_PYTHON ?? "")), + binaryLayoutEnabled: + options.binaryLayout !== undefined || + (process.platform === "linux" && + process.arch === "x64" && + isAbsolute(process.env.REA_PWNTOOLS_PYTHON ?? "")), firmwareInspectionEnabled: options.firmwareAnalysis !== undefined || (process.platform === "linux" && @@ -186,6 +215,24 @@ export const createServer = ( toolLogger, recordEvidence, ); + registerBinaryDiagnosticsTools( + server, + options.binaryLayout ?? createBinaryLayoutService(), + toolLogger, + recordEvidence, + ); + registerEvmTools( + server, + options.evmInterface ?? createEvmInterfaceService(), + toolLogger, + recordEvidence, + ); + registerRecordedCrashTools( + server, + options.recordedCrash ?? createRecordedCrashService(), + toolLogger, + recordEvidence, + ); registerFirmwareTools( server, new FirmwareAnalysisService( @@ -213,6 +260,12 @@ export const createServer = ( toolLogger, recordEvidence, ); + registerWebNetworkCaptureTool( + server, + options.webNetworkCapture ?? createWebNetworkCaptureService(), + toolLogger, + recordEvidence, + ); registerJavaScriptRecoveryTool( server, new JavaScriptRecoveryService( diff --git a/src/server/promptCompletion.ts b/src/server/promptCompletion.ts index b02c5a35ba..bb51930099 100644 --- a/src/server/promptCompletion.ts +++ b/src/server/promptCompletion.ts @@ -7,7 +7,7 @@ import type { BinarySessionPort } from "../application/binary/BinarySession.js"; import type { PromptCompletionKind } from "../contracts/promptContracts.js"; import { artifactInventoryResultSchema } from "../domain/artifactGraph.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; -import { processCaptureSchema } from "../domain/processCapture.js"; +import { parseProcessCapture } from "../domain/process/processCapture.js"; const documentListSchema = z.array(z.string().min(1)); const providerStatusSchema = z.object({ @@ -171,8 +171,13 @@ const evidenceValues = ( const isProcessCaptureEvidence = (evidence: LedgerEvidence): boolean => { if (evidence.operation !== "capture_process_scenario") return false; - const capture = processCaptureSchema.safeParse(evidence.normalized_result); - return capture.success; + try { + parseProcessCapture(evidence.normalized_result); + return true; + } catch (cause: unknown) { + if (cause instanceof TypeError || cause instanceof z.ZodError) return false; + throw cause; + } }; const unknownCandidates = ( diff --git a/src/server/registerBinaryDiagnosticsTools.ts b/src/server/registerBinaryDiagnosticsTools.ts new file mode 100644 index 0000000000..949e24624a --- /dev/null +++ b/src/server/registerBinaryDiagnosticsTools.ts @@ -0,0 +1,32 @@ +import type { McpServer } from "@modelcontextprotocol/server"; +import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import type { Logger } from "../logger.js"; +import { logToolExecution } from "./toolLogging.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { toCallToolResult } from "./toolResult.js"; + +/** Bind offline layout inspection to its named contract and session Evidence owner. */ +export const registerBinaryDiagnosticsTools = ( + server: McpServer, + service: BinaryLayoutService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], +): void => { + const contract = toolContract("inspect_binary_layout"); + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + async (input, context) => { + const result = await logToolExecution(logger, contract.name, () => + service.inspect(input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) return toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return recorded !== undefined && !recorded.ok + ? toCallToolResult(recorded, contract) + : toCallToolResult(result, contract); + }, + ); +}; diff --git a/src/server/registerEvidenceTools.ts b/src/server/registerEvidenceTools.ts index e906bd8624..be678179b2 100644 --- a/src/server/registerEvidenceTools.ts +++ b/src/server/registerEvidenceTools.ts @@ -14,7 +14,7 @@ import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { toCallToolResult } from "./toolResult.js"; -import { createArtifactExtractionDestination } from "../application/ArtifactExtractionDestination.js"; +import { createArtifactExtractionDestination } from "../application/artifacts/ArtifactExtractionDestination.js"; interface EvidenceToolRegistration { readonly logger: Logger; diff --git a/src/server/registerEvmTools.ts b/src/server/registerEvmTools.ts new file mode 100644 index 0000000000..09af2dc26c --- /dev/null +++ b/src/server/registerEvmTools.ts @@ -0,0 +1,32 @@ +import type { McpServer } from "@modelcontextprotocol/server"; +import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import type { Logger } from "../logger.js"; +import { logToolExecution } from "./toolLogging.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { toCallToolResult } from "./toolResult.js"; + +/** Bind the named EVM contract to shared validation and session Evidence recording. */ +export const registerEvmTools = ( + server: McpServer, + service: EvmInterfaceService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], +): void => { + const contract = toolContract("inspect_evm_interface"); + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + async (input, context) => { + const result = await logToolExecution(logger, contract.name, () => + service.inspect(input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) return toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return recorded !== undefined && !recorded.ok + ? toCallToolResult(recorded, contract) + : toCallToolResult(result, contract); + }, + ); +}; diff --git a/src/server/registerProcessComparisonTool.ts b/src/server/registerProcessComparisonTool.ts index 8685283229..3b47d9c423 100644 --- a/src/server/registerProcessComparisonTool.ts +++ b/src/server/registerProcessComparisonTool.ts @@ -13,7 +13,7 @@ import { jsonValueSchema } from "../domain/jsonValue.js"; import { compareProcessCaptures, parseProcessCapture, -} from "../domain/processCapture.js"; +} from "../domain/process/processCapture.js"; import type { RecordUnknownInput } from "../domain/residualUnknown.js"; import { err } from "../domain/result.js"; import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; diff --git a/src/server/registerRecordedCrashTools.ts b/src/server/registerRecordedCrashTools.ts new file mode 100644 index 0000000000..0381689a0f --- /dev/null +++ b/src/server/registerRecordedCrashTools.ts @@ -0,0 +1,32 @@ +import type { McpServer } from "@modelcontextprotocol/server"; +import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import type { Logger } from "../logger.js"; +import { logToolExecution } from "./toolLogging.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { toCallToolResult } from "./toolResult.js"; + +/** Bind recorded crash inspection to its named contract and session Evidence owner. */ +export const registerRecordedCrashTools = ( + server: McpServer, + service: RecordedCrashService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], +): void => { + const contract = toolContract("inspect_recorded_crash"); + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + async (input, context) => { + const result = await logToolExecution(logger, contract.name, () => + service.inspect(input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) return toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return recorded !== undefined && !recorded.ok + ? toCallToolResult(recorded, contract) + : toCallToolResult(result, contract); + }, + ); +}; diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts index fd601bfcfb..fb6feb548f 100644 --- a/src/server/registerSessionTools.ts +++ b/src/server/registerSessionTools.ts @@ -7,14 +7,14 @@ import { } from "../application/AnalysisContextQueries.js"; import { readAnalysisSnapshot } from "../application/binary/AnalysisSnapshotFiles.js"; import type { BinarySessionPort } from "../application/binary/BinarySession.js"; -import { createProcessCaptureEvidence } from "../application/ProcessEvidence.js"; -import { captureProcessScenario } from "../application/ProcessHarness.js"; +import { createProcessCaptureEvidence } from "../application/process/ProcessEvidence.js"; +import { captureProcessScenario } from "../process/capture/ProcessHarness.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js"; import { UnknownRegistryError } from "../domain/unknownRegistryError.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; import type { Evidence } from "../domain/evidence.js"; -import type { ProcessCapture } from "../domain/processCapture.js"; +import type { ProcessCapture } from "../domain/process/processCapture.js"; import { ok, type Result } from "../domain/result.js"; import type { Logger } from "../logger.js"; import { mcpProgressReporter } from "./mcpProgress.js"; diff --git a/src/server/registerWebNetworkCaptureTool.ts b/src/server/registerWebNetworkCaptureTool.ts new file mode 100644 index 0000000000..2c1a9e5c58 --- /dev/null +++ b/src/server/registerWebNetworkCaptureTool.ts @@ -0,0 +1,46 @@ +import type { + McpServer, + StandardSchemaWithJSON, +} from "@modelcontextprotocol/server"; +import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import type { Logger } from "../logger.js"; +import { logToolExecution } from "./toolLogging.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { toCallToolResult } from "./toolResult.js"; + +/** Bind historical inspection to its named contract and caller-owned Evidence writer. */ +export const registerWebNetworkCaptureTool = ( + server: McpServer, + service: WebNetworkCaptureService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], +): void => { + const contract = toolContract("inspect_web_network_capture"); + const registration = toolRegistrationOptions(contract); + // Keep the exact advertised contract; the shared service validates raw input + // before effects so caller-declared sensitivity also covers invalid arguments. + // SDK pre-validation otherwise emits unprojected issue paths as plain text. + const inputSchema: StandardSchemaWithJSON = { + "~standard": { + ...registration.inputSchema["~standard"], + vendor: "rea-application", + validate: (value: unknown) => ({ value }), + }, + }; + server.registerTool( + contract.name, + { ...registration, inputSchema }, + async (input, context) => { + const result = await logToolExecution(logger, contract.name, () => + service.inspect(input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) return toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return recorded !== undefined && !recorded.ok + ? toCallToolResult(recorded, contract) + : toCallToolResult(result, contract); + }, + ); +}; diff --git a/src/server/sessionAvailabilityPolicy.ts b/src/server/sessionAvailabilityPolicy.ts index 904594c468..829102d648 100644 --- a/src/server/sessionAvailabilityPolicy.ts +++ b/src/server/sessionAvailabilityPolicy.ts @@ -10,6 +10,7 @@ export interface SessionAvailabilityDefaults { | undefined; readonly optionalFeatures?: Pick< SessionAvailability, + | "evmInterfaceEnabled" | "browserObservationEnabled" | "browserScenarioEnabled" | "electronObservationEnabled" @@ -18,6 +19,8 @@ export interface SessionAvailabilityDefaults { | "androidAnalysisEnabled" | "javascriptRecoveryEnabled" | "webModuleResolutionEnabled" + | "binaryLayoutEnabled" + | "recordedCrashEnabled" | "firmwareInspectionEnabled" | "firmwareExtractionEnabled" >; @@ -31,7 +34,13 @@ export const sessionAvailabilityPolicy = ( const policy = configured ?? (() => ({ + evmInterfaceEnabled: + defaults.optionalFeatures?.evmInterfaceEnabled ?? false, processCaptureEnabled: platform !== "win32", + recordedCrashEnabled: + defaults.optionalFeatures?.recordedCrashEnabled ?? false, + binaryLayoutEnabled: + defaults.optionalFeatures?.binaryLayoutEnabled ?? false, firmwareInspectionEnabled: defaults.optionalFeatures?.firmwareInspectionEnabled ?? false, firmwareExtractionEnabled: diff --git a/src/server/sessionToolPolicies.ts b/src/server/sessionToolPolicies.ts index be151c59e1..b1725b0975 100644 --- a/src/server/sessionToolPolicies.ts +++ b/src/server/sessionToolPolicies.ts @@ -1,4 +1,4 @@ -export { PROCESS_PROVIDER } from "../application/ProcessEvidence.js"; +export { PROCESS_PROVIDER } from "../application/process/ProcessEvidence.js"; export { ARTIFACT_COMPARISON_PROVIDER, BUNDLE_COMPARISON_PROVIDER, diff --git a/src/server/toolRegistrationOptions.ts b/src/server/toolRegistrationOptions.ts index 100648d5c4..0f0b926796 100644 --- a/src/server/toolRegistrationOptions.ts +++ b/src/server/toolRegistrationOptions.ts @@ -1,5 +1,8 @@ import type { ToolContract } from "../contracts/toolContracts.js"; -import { toolInputSchemaWithMetadata } from "../contracts/toolSchemaMetadata.js"; +import { + toolInputSchemaWithMetadata, + toolOutputSchemaWithMetadata, +} from "../contracts/toolSchemaMetadata.js"; /** Project the canonical Zod contracts directly into SDK registration. */ export const toolRegistrationOptions = ( @@ -14,6 +17,6 @@ export const toolRegistrationOptions = ( title: contract.title, description: contract.description, inputSchema: toolInputSchemaWithMetadata(contract), - outputSchema: contract.outputSchema, + outputSchema: toolOutputSchemaWithMetadata(contract), annotations: contract.annotations, }); diff --git a/src/server/toolResult.ts b/src/server/toolResult.ts index f74dfe32d1..ca2319bdd5 100644 --- a/src/server/toolResult.ts +++ b/src/server/toolResult.ts @@ -9,7 +9,7 @@ import type { Result } from "../domain/result.js"; /** * Serialize an application result as MCP text and structured content. * Shared error projection preserves actionable local diagnostics while omitting - * raw causes and captured process output. + * raw cause objects; explicitly retained bounded command output stays visible. */ export const toCallToolResult = ( result: Result, diff --git a/tests/acceptance/applications/applicationEvidenceMcp.test.ts b/tests/acceptance/applications/applicationEvidenceMcp.test.ts index 3fdba32f4d..6b21ac22c9 100644 --- a/tests/acceptance/applications/applicationEvidenceMcp.test.ts +++ b/tests/acceptance/applications/applicationEvidenceMcp.test.ts @@ -1,9 +1,7 @@ -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import type { Client } from "@modelcontextprotocol/client"; import { describe, expect, it } from "vitest"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; -import { createServer } from "../../../src/server/createServer.js"; import { JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE, JAVASCRIPT_FEATURE_TRACE_EXAMPLE, @@ -11,39 +9,10 @@ import { SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, } from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; import { createEvidence } from "../../../src/domain/evidence.js"; -import { observed } from "../../fixtures/analysisExecution.js"; - -interface TestHarness { - readonly client: Client; - readonly session: BinarySession; - readonly close: () => Promise; -} - -async function createTestHarness(): Promise { - const session = createTestBinarySession(() => ({ - execute: () => Promise.resolve(observed(null)), - close: () => Promise.resolve(), - })); - const server = createServer(session, session); - const client = new Client({ - name: "application-workflow-test", - version: "1", - }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await server.connect(serverTransport); - await client.connect(clientTransport); - - return { - client, - session, - close: async () => { - await client.close(); - await server.close(); - await session.close(); - }, - }; -} +import { + createApplicationMcpHarness, + type ApplicationMcpHarness, +} from "./mcpHarness.js"; async function runInlineEvidenceScenarios( client: Client, @@ -103,7 +72,9 @@ async function runInlineEvidenceScenarios( expect(session.exportEvidenceBundle().records.length).toBeGreaterThan(2); } -async function runInlineWorkflowScenarios(harness: TestHarness): Promise { +async function runInlineWorkflowScenarios( + harness: ApplicationMcpHarness, +): Promise { const tracedInline = await harness.client.callTool({ name: "trace_application_feature", arguments: { @@ -250,7 +221,7 @@ async function assertRejectedInlineEvidence(client: Client): Promise { describe("application workflow MCP parity", () => { it("traces and compares authenticated graph Evidence in the session", async () => { - const harness = await createTestHarness(); + const harness = await createApplicationMcpHarness(); try { await runInlineEvidenceScenarios(harness.client, harness.session); await runInlineWorkflowScenarios(harness); diff --git a/tests/acceptance/applications/applicationExportShapeMcp.test.ts b/tests/acceptance/applications/applicationExportShapeMcp.test.ts index c104c8012d..4fad716155 100644 --- a/tests/acceptance/applications/applicationExportShapeMcp.test.ts +++ b/tests/acceptance/applications/applicationExportShapeMcp.test.ts @@ -1,15 +1,14 @@ -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { describe, expect, it } from "vitest"; -import { copyFile, mkdir, rm } from "node:fs/promises"; +import { copyFile, mkdir, rm, writeFile } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { createServer } from "../../../src/server/createServer.js"; -import { observed } from "../../fixtures/analysisExecution.js"; import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js"; +import { compareJavaScriptExportShapesEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; +import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; +import { createApplicationMcpHarness } from "./mcpHarness.js"; describe("application workflow MCP parity", () => { it("compares exact parser export shapes with inline Evidence", async () => { @@ -40,14 +39,8 @@ describe("application workflow MCP parity", () => { const leftAnalysis = javascriptApplicationAnalysisResultSchema.parse( left.value.normalized_result, ); - const session = createTestBinarySession(() => ({ - execute: () => Promise.resolve(observed(null)), - close: () => Promise.resolve(), - })); - const server = createServer(session, session); - const client = new Client({ name: "export-shape-test", version: "1" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const harness = await createApplicationMcpHarness(); + const { client, session } = harness; const selectors = { left_module_path: "parser.mjs", left_export_name: "default", @@ -55,8 +48,6 @@ describe("application workflow MCP parity", () => { right_export_name: "default", }; try { - await server.connect(serverTransport); - await client.connect(clientTransport); const relativeApplication = await client.callTool({ name: "analyze_javascript_application", arguments: { input_path: relative(process.cwd(), leftRoot) }, @@ -121,10 +112,114 @@ describe("application workflow MCP parity", () => { }, }); } finally { - await client.close(); - await server.close(); - await session.close(); + await harness.close(); await rm(root, { recursive: true, force: true }); } }); }); + +describe("source-produced export comparison inventories", () => { + it("retains large source-produced export inventories through comparison schemas", async () => { + const fields = Array.from( + { length: 64 }, + (_, index) => `field${String(index)}: ${String(index)}`, + ).join(", "); + const returnSites = Array.from( + { length: 33 }, + (_, index) => + `if (value === ${String(index)}) return { type: "variant-${String(index)}", ${fields} };`, + ).join("\n"); + const extraExports = Array.from( + { length: 1_000 }, + (_, index) => + `export const candidate${String(index)} = ${String(index)};`, + ).join("\n"); + const inventorySource = `export default function parse(value) {\n${returnSites}\n throw new Error("no match");\n}\n${extraExports}`; + const pairedVariantsSource = `export default function parse(value) { +${returnSites} + throw new Error("no match"); +}`; + const [inventoryLeft, inventoryRight] = await Promise.all([ + analyzeSourceEvidence(inventorySource), + analyzeSourceEvidence(pairedVariantsSource), + ]); + const variants = compareEvidence(inventoryLeft, inventoryRight); + expect(variants.left).toMatchObject({ status: "selected" }); + expect(variants.coverage).toMatchObject({ + paired_variants: 33, + unpaired_left_variants: 0, + unpaired_right_variants: 0, + left_source_omitted_variants: 0, + right_source_omitted_variants: 0, + left_omitted_fields: 0, + right_omitted_fields: 0, + }); + + const missing = compareEvidence(inventoryLeft, inventoryRight, { + leftExportName: "missing", + }); + expect(missing.left).toMatchObject({ + status: "missing", + omitted_candidates: 0, + }); + expect(missing.left.candidates).toHaveLength(1_001); + + const changedFieldsLeft = Array.from( + { length: 10_001 }, + (_, index) => `field${String(index)}: ${String(index)}`, + ).join(", "); + const changedFieldsRight = Array.from( + { length: 10_001 }, + (_, index) => `field${String(index)}: ${String(index + 1)}`, + ).join(", "); + const [changeLeft, changeRight] = await Promise.all([ + analyzeSourceEvidence( + `export default () => ({ type: "record", ${changedFieldsLeft} });`, + ), + analyzeSourceEvidence( + `export default () => ({ type: "record", ${changedFieldsRight} });`, + ), + ]); + const changes = compareEvidence(changeLeft, changeRight); + expect(changes.changes).toHaveLength(10_001); + expect(changes.summary).toEqual({ + added: 0, + removed: 0, + changed: 10_001, + unknown: 0, + }); + expect(changes.coverage).toMatchObject({ + status: "complete-within-inputs", + omitted_changes: 0, + left_omitted_fields: 0, + right_omitted_fields: 0, + }); + }, 30_000); +}); + +const analyzeSourceEvidence = async (source: string) => { + const root = await createTestTempDirectory("rea-export-shape-inventory-"); + await writeFile(join(root, "parser.mjs"), source); + const analyzed = await analyzeJavaScriptApplication({ input_path: root }); + if (!analyzed.ok) throw analyzed.error; + return analyzed.value; +}; + +const compareEvidence = ( + left: Awaited>, + right: Awaited>, + selectors: { readonly leftExportName?: string } = {}, +) => { + const compared = compareJavaScriptExportShapesEvidence({ + left, + right, + left_module_path: "parser.mjs", + left_export_name: selectors.leftExportName ?? "default", + right_module_path: "parser.mjs", + right_export_name: "default", + }); + if (!compared.ok) throw compared.error; + return javaScriptExportShapeComparisonResultSchema.parse( + compared.value.normalized_result, + ); +}; diff --git a/tests/acceptance/applications/electronBlankPathCli.test.ts b/tests/acceptance/applications/electronBlankPathCli.test.ts new file mode 100644 index 0000000000..f060aed34d --- /dev/null +++ b/tests/acceptance/applications/electronBlankPathCli.test.ts @@ -0,0 +1,32 @@ +import { expect } from "vitest"; + +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for(["", " \t "])( + "rejects blank Electron application_root (%j) before filesystem or provider work", + async (applicationRoot, { cli }) => { + const response = await cli.run({ + arguments: [ + "capture-electron-scenario", + JSON.stringify({ + executable_path: "/synthetic/missing-electron", + application_path: "/synthetic/missing-app", + application_root: applicationRoot, + }), + "--json", + ], + environment: { REA_LOG_LEVEL: "silent" }, + timeoutMs: 15000, + }); + expect(response.exitCode).toBe(1); + expect(response.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "capture_electron_scenario", + issues: expect.arrayContaining([ + { path: ["application_root"], reason: "out_of_range", minimum: 1 }, + ]), + }, + }); + }, +); diff --git a/tests/acceptance/applications/mcpHarness.ts b/tests/acceptance/applications/mcpHarness.ts new file mode 100644 index 0000000000..5f971e4774 --- /dev/null +++ b/tests/acceptance/applications/mcpHarness.ts @@ -0,0 +1,39 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; + +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; +import { observed } from "../../fixtures/analysisExecution.js"; +import { createServer } from "../../../src/server/createServer.js"; + +/** Owned MCP SDK session shared by application workflow scenarios. */ +export interface ApplicationMcpHarness { + readonly client: Client; + readonly session: BinarySession; + readonly close: () => Promise; +} + +/** Connect application workflows through the production MCP registration. */ +export async function createApplicationMcpHarness(): Promise { + const session = createTestBinarySession(() => ({ + execute: () => Promise.resolve(observed(null)), + close: () => Promise.resolve(), + })); + const server = createServer(session, session); + const client = new Client({ + name: "application-workflow-test", + version: "1", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await server.connect(serverTransport); + await client.connect(clientTransport); + + return { + client, + session, + close: async () => { + await Promise.all([client.close(), server.close(), session.close()]); + }, + }; +} diff --git a/tests/acceptance/applications/runtime.test.ts b/tests/acceptance/applications/runtime.test.ts index 49bf0975fb..d7aff2937d 100644 --- a/tests/acceptance/applications/runtime.test.ts +++ b/tests/acceptance/applications/runtime.test.ts @@ -17,12 +17,13 @@ const fixturePath = fileURLToPath( new URL("../../fixtures/fakeLauncher.mjs", import.meta.url), ); -const expectAvailableToolInventory = async (client: Client): Promise => { +const expectAvailableToolInventory = async (client: Client) => { const listed = await client.listTools(); const status = await client.callTool({ name: "binary_session", arguments: {}, }); + expect(status.isError).not.toBe(true); const availability = z .object({ result: z.object({ @@ -43,6 +44,7 @@ const expectAvailableToolInventory = async (client: Client): Promise => { expect(new Set(listed.tools.map(({ name }) => name))).toEqual( new Set(TOOL_CONTRACTS.map(({ name }) => name)), ); + return status; }; /** @@ -57,72 +59,83 @@ const completeStderrRecords = (stderr: string): unknown[] => .filter((line) => line.length > 0) .map((line): unknown => JSON.parse(line)); +const callCurrentDocument = async (target?: { + readonly path: string; + readonly kind?: "database"; +}) => { + const transport = new StdioClientTransport({ + command: process.execPath, + args: [mainPath], + cwd: process.cwd(), + env: { + PATH: process.env.PATH ?? "", + HOPPER_LAUNCHER_PATH: process.execPath, + ...(target === undefined + ? {} + : { + HOPPER_TARGET_PATH: target.path, + ...(target.kind === undefined + ? {} + : { HOPPER_TARGET_KIND: target.kind }), + }), + HOPPER_LOADER_ARGS_JSON: JSON.stringify([fixturePath]), + }, + stderr: "pipe", + }); + let stderr = ""; + transport.stderr?.on("data", (chunk: Buffer | string) => { + stderr += chunk.toString(); + }); + const client = new Client({ name: "runtime-smoke", version: "1.0.0" }); + + let result: Awaited>; + try { + await client.connect(transport); + result = await client.callTool({ + name: "current_document", + arguments: {}, + }); + } finally { + await client.close(); + await transport.close(); + } + return { result, stderr, records: completeStderrRecords(stderr) }; +}; + describe("production stdio runtime", () => { // The `test:acceptance` lane runs the real entrypoint on the current host; // these cases state each supported host's result in the test name and body. - const callCurrentDocument = async () => { - const transport = new StdioClientTransport({ - command: process.execPath, - args: [mainPath], - cwd: process.cwd(), - env: { - PATH: process.env.PATH ?? "", - HOPPER_LAUNCHER_PATH: process.execPath, - HOPPER_TARGET_PATH: process.execPath, - HOPPER_LOADER_ARGS_JSON: JSON.stringify([fixturePath]), - }, - stderr: "pipe", - }); - let stderr = ""; - transport.stderr?.on("data", (chunk: Buffer | string) => { - stderr += chunk.toString(); - }); - const client = new Client({ name: "runtime-smoke", version: "1.0.0" }); - let result: Awaited>; - try { - await client.connect(transport); - await expectAvailableToolInventory(client); - result = await client.callTool({ - name: "current_document", - arguments: {}, - }); - } finally { - await client.close(); - await transport.close(); - } - return { result, stderr, records: completeStderrRecords(stderr) }; - }; - - it.runIf(process.platform === "linux")( - "reports the Linux current-document failure through MCP and structured logs", - async () => { - const { result, stderr, records } = await callCurrentDocument(); - expect(result.isError).toBe(true); - expect(result.content).toEqual( - expect.arrayContaining([ - expect.objectContaining({ type: "text", text: expect.any(String) }), - ]), - ); - expect(records).toContainEqual( + it("reports a missing target through MCP and structured logs", async () => { + const { result, stderr, records } = await callCurrentDocument(); + expect(result.isError).toBe(true); + expect(result.content).toEqual( + expect.arrayContaining([ expect.objectContaining({ - application: "rea", - mode: "mcp", - layer: "server", - tool: "current_document", - status: "error", + type: "text", + text: expect.stringContaining("No app is open."), }), - ); - expect(stderr).not.toContain("HOPPER_LOADER_ARGS_JSON"); - expect(stderr).not.toContain(fixturePath); - }, - 15_000, - ); + ]), + ); + expect(records).toContainEqual( + expect.objectContaining({ + application: "rea", + mode: "mcp", + layer: "server", + tool: "current_document", + status: "error", + }), + ); + expect(stderr).not.toContain("HOPPER_LOADER_ARGS_JSON"); + expect(stderr).not.toContain(fixturePath); + }, 15_000); it.runIf(process.platform === "darwin")( "returns the selected document on macOS through MCP and structured logs", async () => { - const { result, stderr, records } = await callCurrentDocument(); + const { result, stderr, records } = await callCurrentDocument({ + path: process.execPath, + }); expect(result.isError).not.toBe(true); const observation = evidenceResultOf(z.literal("fixture")).parse( result.structuredContent, @@ -146,17 +159,13 @@ describe("production stdio runtime", () => { 15_000, ); - it("starts with a database-kind initial target without a fatal record", async () => { + it("serves the compiled catalog without an initial target or fatal record", async () => { const transport = new StdioClientTransport({ command: process.execPath, args: [mainPath], cwd: process.cwd(), env: { PATH: process.env.PATH ?? "", - HOPPER_LAUNCHER_PATH: process.execPath, - HOPPER_TARGET_PATH: new URL(import.meta.url).pathname, - HOPPER_TARGET_KIND: "database", - HOPPER_LOADER_ARGS_JSON: JSON.stringify([fixturePath]), }, stderr: "pipe", }); @@ -164,23 +173,24 @@ describe("production stdio runtime", () => { transport.stderr?.on("data", (chunk: Buffer | string) => { stderr += chunk.toString(); }); - const client = new Client({ name: "database-runtime", version: "1.0.0" }); + const client = new Client({ name: "catalog-runtime", version: "1.0.0" }); try { await client.connect(transport); - // The catalog itself is already proven against the compiled entrypoint - // above, so this test asserts the part that is unique to a database-kind - // initial target: the server comes up and serves tools without a fatal - // startup record. - const listed = await client.listTools(); - expect(listed.tools.length).toBeGreaterThan(0); - // A database-kind initial target must also serve a tool. Merely listing - // the catalog starts no bridge work, so without this call the child - // writes no startup records at all and the fatal-record assertion below - // would pass without observing anything. - await expect( - client.callTool({ name: "current_document", arguments: {} }), - ).resolves.toBeDefined(); + // Keep catalog parity independent of initial target/provider startup. + const status = await expectAvailableToolInventory(client); + expect( + z + .object({ result: z.object({ open: z.boolean() }) }) + .parse(status.structuredContent).result, + ).toEqual({ open: false }); + // Closing the empty session produces a structured lifecycle record, + // making the fatal-record check observable without launching a provider. + const closed = await client.callTool({ + name: "close_binary", + arguments: {}, + }); + expect(closed.isError).not.toBe(true); } finally { await client.close(); await transport.close(); @@ -194,6 +204,9 @@ describe("production stdio runtime", () => { records.length, "no startup records were observed, so the fatal-record check is vacuous", ).toBeGreaterThan(0); + expect(records).toContainEqual( + expect.objectContaining({ tool: "close_binary", status: "ok" }), + ); const fatal = records.filter( (record): record is { level: number } => typeof record === "object" && @@ -204,4 +217,30 @@ describe("production stdio runtime", () => { ); expect(fatal).toEqual([]); }, 15_000); + + it.runIf(process.platform === "darwin")( + "serves a database-kind initial target without a fatal record on macOS", + async () => { + const { result, records } = await callCurrentDocument({ + path: fileURLToPath(import.meta.url), + kind: "database", + }); + expect(result.isError).not.toBe(true); + evidenceResultOf(z.literal("fixture")).parse(result.structuredContent); + expect(records).toContainEqual( + expect.objectContaining({ tool: "current_document", status: "ok" }), + ); + expect( + records.some( + (record) => + typeof record === "object" && + record !== null && + "level" in record && + typeof record.level === "number" && + record.level >= 50, + ), + ).toBe(false); + }, + 15_000, + ); }); diff --git a/tests/acceptance/nonBinaryComposition.test.ts b/tests/acceptance/nonBinaryComposition.test.ts index a5229ebf0e..f7c28fa410 100644 --- a/tests/acceptance/nonBinaryComposition.test.ts +++ b/tests/acceptance/nonBinaryComposition.test.ts @@ -89,6 +89,24 @@ cliTest.skipIf(process.platform !== "linux")( }, ); +cliTest.skipIf(process.platform !== "linux")( + "resolves relative firmware CLI paths against the operator working directory", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-firmware-cli-relative-"); + await writeFile(join(root, "input.bin"), "firmware fixture"); + const cliResult = await cli.run({ + arguments: ["inspect-firmware-regions", "input.bin", "--json"], + cwd: root, + environment: { REA_LOG_LEVEL: "silent" }, + timeoutMs: 10_000, + }); + // A relative path must pass input validation and reach the provider, + // which reports the missing firmware engine — not a validation error. + expect(cliResult.exitCode).toBe(1); + expect(cliResult.json).toMatchObject({ code: "capability_unavailable" }); + }, +); + cliTest( "preserves optional JavaScript recovery configuration through CLI and MCP", async ({ cli }) => { diff --git a/tests/acceptance/webModuleCli.test.ts b/tests/acceptance/webModuleCli.test.ts index fc4d7bd88f..5bab64449f 100644 --- a/tests/acceptance/webModuleCli.test.ts +++ b/tests/acceptance/webModuleCli.test.ts @@ -24,7 +24,7 @@ cliTest( join(root, "files", "modules", "main.js"), artifacts.source, ); - const manifestPath = join(root, "manifest.json"); + const manifestPath = join(root, "manifest.json").replaceAll("\\", "/"); await writeFile(manifestPath, JSON.stringify(manifest)); const environment = { REA_LOG_LEVEL: "silent", diff --git a/tests/acceptance/webSourceLocationCli.test.ts b/tests/acceptance/webSourceLocationCli.test.ts index c7ca688e2d..2e7b3a2fe3 100644 --- a/tests/acceptance/webSourceLocationCli.test.ts +++ b/tests/acceptance/webSourceLocationCli.test.ts @@ -1,4 +1,10 @@ -import { expect } from "vitest"; +import { expect, onTestFinished } from "vitest"; +import { Client } from "@modelcontextprotocol/client"; +import { + StdioClientTransport, + getDefaultEnvironment, +} from "@modelcontextprotocol/client/stdio"; +import { fileURLToPath } from "node:url"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { webSourceLocationFixture } from "../fixtures/webSourceLocation.js"; @@ -12,7 +18,7 @@ cliTest( const fixture = webSourceLocationFixture(); await mkdir(join(root, "files", "modules"), { recursive: true }); await writeFile(join(root, "files", "modules", "main.js"), fixture.source); - const manifest = join(root, "manifest.json"); + const manifest = join(root, "manifest.json").replaceAll("\\", "/"); const map = join(root, "app.js.map"); await writeFile(manifest, JSON.stringify(fixture.manifest)); await writeFile(map, fixture.sourceMap.text); @@ -44,6 +50,40 @@ cliTest( runtime: { id: "node-v8" }, }, }); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [ + fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url)), + "mcp", + ], + env: { ...getDefaultEnvironment(), REA_LOG_LEVEL: "silent" }, + stderr: "pipe", + }); + const client = new Client({ + name: "source-location-cli-parity", + version: "1", + }); + onTestFinished(async () => { + try { + await client.close(); + } finally { + await transport.close(); + } + }); + await client.connect(transport); + const mcpResponse = await client.callTool({ + name: "trace_web_source_location", + arguments: { + manifest_path: manifest, + script_index: 0, + source_map: { path: map, url: fixture.sourceMap.url }, + generated_position: { line: 1, column: 0 }, + }, + }); + expect(mcpResponse.isError, JSON.stringify(mcpResponse)).not.toBe(true); + expect(mcpResponse.structuredContent).toMatchObject({ + evidence: response.json, + }); const invalid = await cli.run({ arguments: [ "trace-web-source-location", diff --git a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts new file mode 100644 index 0000000000..702458224a --- /dev/null +++ b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts @@ -0,0 +1,691 @@ +import { + createTestWorkspace, + removeTestWorkspace, +} from "../../support/workspace/workspaceFixture.js"; +import { access, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { z } from "zod"; +import { expect, it, onTestFinished } from "vitest"; +import { PwntoolsLayoutProvider } from "../../../src/native/pwntools/PwntoolsLayoutProvider.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; +import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; +import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; +import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; +import { binaryLayoutFixture } from "../../fixtures/binaryDiagnostics/layout.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; + +const unsupportedHost = process.platform !== "linux" || process.arch !== "x64"; +const requestSchema = z.strictObject({ + snapshot_path: z.string(), + reply_path: z.string(), +}); + +it.runIf(!unsupportedHost).each([ + ["SIGXCPU", "reported", "cpu"], + ["SIGXCPU", "missing", "cpu"], + ["SIGXCPU", "malformed", "cpu"], + ["SIGXFSZ", "reported", "file-size"], + ["EFBIG", "reported", "file-size"], + ["EFBIG", "malformed", "file-size"], +] as const)( + "preserves observed %s with %s limit evidence", + async (failure, scenario, resource) => { + const { path } = await fixture(); + let ownedPath = ""; + const limits = { + address_space_bytes: 3221225472, + cpu_seconds: 1, + file_size_bytes: 1024, + }; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + ownedPath = spawn.cwd ?? ""; + if (failure === "EFBIG") + await writeFile(join(ownedPath, "resource.failure"), "F"); + if (scenario !== "missing") + await writeFile( + join(ownedPath, "limits.json"), + scenario === "reported" + ? JSON.stringify(limits) + : '{"cpu_seconds":999}', + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + failure === "EFBIG" + ? 'process.stdout.write("resource diagnostic", () => { process.exitCode = 76; })' + : failure === "SIGXFSZ" + ? 'process.on("SIGXFSZ", () => { process.removeAllListeners("SIGXFSZ"); process.kill(process.pid, "SIGXFSZ"); }); process.stdout.write("resource diagnostic", () => process.kill(process.pid, "SIGXFSZ")); setTimeout(() => {}, 1000);' + : `process.stdout.write("resource diagnostic", () => process.kill(process.pid, "${failure}"))`, + ], + }); + }, + ); + const result = await provider.inspect({ path }); + if (result.ok) throw new Error("Expected observed resource failure"); + const projected = projectAnalysisError(result.error); + expect(projected).toMatchObject({ + code: "resource_constraint", + details: { + resource, + reported_limits: scenario === "reported" ? limits : null, + captured_output: { stdout: "resource diagnostic", truncated: false }, + }, + }); + expect(result.error.message).toContain( + failure === "EFBIG" + ? "exact write failure cause is unknown" + : "exact signal cause is unknown", + ); + if (scenario !== "reported") + expect(result.error.message).toContain("limit report unavailable"); + expect(projected.remediation.action).toContain( + resource === "cpu" ? "CPU" : "file-size", + ); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost)( + "retains actual diagnostic truncation when output overflow and process cleanup both fail", + async () => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + ownedPath = spawn.cwd ?? ""; + const launched = await spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", 'process.stdout.write("x".repeat(1048577))'], + }); + return { + ...launched, + cleanup: async () => { + await launched.cleanup?.(); + throw new Error("injected post-cleanup reporting failure"); + }, + }; + }, + ); + const result = await provider.inspect({ path }); + if (result.ok) throw new Error("Expected compounded lifecycle failure"); + expect(result.error).toMatchObject({ + cleanupIncomplete: true, + diagnostics: { + reason: "injected post-cleanup reporting failure", + previous_error: { failure_kind: "output-limit" }, + captured_output: { truncated: true, stderr: "" }, + }, + }); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it + .runIf(!unsupportedHost) + .each([ + "reserved-memory-status", + "unmarked-memory-status", + "unmarked-file-status", + "wrong-marker", + "ordinary-exit", + "signal", + ])( + "requires matching private bridge failure evidence, without guessing from stderr: %s", + async (scenario) => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + ownedPath = spawn.cwd ?? ""; + if ( + scenario === "reserved-memory-status" || + scenario === "wrong-marker" + ) + await writeFile( + join(ownedPath, "resource.failure"), + scenario === "wrong-marker" ? Buffer.from([0xcd]) : "M", + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + 'process.stderr.write("MemoryError-like diagnostic\\n");' + + (scenario === "signal" + ? 'process.kill(process.pid,"SIGKILL")' + : `process.exit(${scenario === "unmarked-file-status" ? "76" : ["reserved-memory-status", "unmarked-memory-status", "wrong-marker"].includes(scenario) ? "75" : "7"})`), + ], + }); + }, + ); + const result = await provider.inspect({ path }); + if (result.ok) throw new Error("Expected explicit process failure"); + const projected = projectAnalysisError(result.error); + expect(projected.code).toBe( + scenario === "reserved-memory-status" + ? "resource_constraint" + : "execution_failure", + ); + expect(JSON.stringify(projected)).toContain("MemoryError-like diagnostic"); + if (scenario === "reserved-memory-status") + expect(projected.details).toMatchObject({ + resource: "memory", + reported_limits: null, + captured_output: { truncated: false }, + }); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each([1048576, 1048577])( + "keeps complete diagnostics or fails explicitly when a valid reply emits %s bytes", + async (bytes) => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + ownedPath = spawn.cwd ?? ""; + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("Request path missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + const { + artifact: _artifact, + diagnostics: _diagnostics, + ...value + } = binaryLayoutFixture(path); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: true, + profile: PWNTOOLS_PROVIDER_IDENTITY.version, + value, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + `process.stdout.write("x".repeat(${String(bytes)}))`, + ], + }); + }, + ); + const result = await provider.inspect({ path }); + if (bytes === 1048576) { + if (!result.ok) throw result.error; + expect(result.value.diagnostics.stdout).toHaveLength(bytes); + expect(result.value.diagnostics.truncated).toBe(false); + } else { + if (result.ok) + throw new Error("Excess diagnostics must not return partial success"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "unreadable_output", + details: { captured_output: { truncated: true } }, + }); + } + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each([ + null, + { + address_space_bytes: 67108864, + cpu_seconds: 20, + file_size_bytes: 33554432, + }, +])( + "preserves reported memory failure, budgets, diagnostics and cleanup: %j", + async (limits) => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + ownedPath = spawn.cwd ?? ""; + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("Request path missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: false, + reason: "resource-limit", + message: "Memory allocation failed; exact cause unknown.", + reported_limits: limits, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + "process.stderr.write('upstream allocation diagnostic')", + ], + }); + }, + ); + const result = await provider.inspect({ path }); + if (result.ok) throw new Error("Expected resource failure"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "resource_constraint", + category: "resource_constraint", + details: { + resource: "memory", + reported_limits: limits, + captured_output: { + stderr: "upstream allocation diagnostic", + truncated: false, + }, + }, + }); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(path, "utf8")).toBe("source-owned-seam-bytes"); + }, +); + +it.runIf(!unsupportedHost).each(["cancelled", "output-limit", "process"])( + "releases an acquired decoder and its snapshot after %s", + async (scenario) => { + const { path } = await fixture(); + const controller = new AbortController(); + let ownedPath = ""; + let pid: number | undefined; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + ownedPath = spawn.cwd ?? ""; + const launched = await spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + scenario === "cancelled" + ? 'process.stdout.write("ready\\n"); setInterval(() => undefined, 1000)' + : scenario === "output-limit" + ? 'process.stdout.write("x".repeat(1048577))' + : 'process.stderr.write("decoder exited unexpectedly"); process.exit(7)', + ], + }); + pid = launched.process.pid; + if (scenario === "cancelled") { + await waitForProviderProcessReady(launched.process); + setImmediate(() => controller.abort()); + } + return launched; + }, + ); + const result = await provider.inspect( + { path }, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: + scenario === "cancelled" + ? "AnalysisCancelledError" + : scenario === "output-limit" + ? "AnalysisOutputError" + : "ProviderAdapterError", + ...(scenario === "process" + ? { + diagnostics: { + exit_code: 7, + stderr: "decoder exited unexpectedly", + }, + } + : {}), + }, + }); + if (pid === undefined) throw new Error("Decoder was not acquired"); + const acquiredPid = pid; + expect(() => process.kill(acquiredPid, 0)).toThrow(); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(path, "utf8")).toBe("source-owned-seam-bytes"); + }, +); + +it.runIf(!unsupportedHost)( + "reports private storage denial as an adapter failure rather than selected-input read denial", + async () => { + const { path } = await fixture(); + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + undefined, + () => + Promise.reject( + Object.assign(new Error("private root denied"), { code: "EACCES" }), + ), + ); + expect(await provider.inspect({ path })).toMatchObject({ + ok: false, + error: { + _tag: "ProviderAdapterError", + diagnostics: { phase: "decoder", reason: "private root denied" }, + }, + }); + }, +); + +it.runIf(!unsupportedHost)( + "preserves a decoder failure when private-root cleanup also fails", + async () => { + const { path, root } = await fixture(); + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("Request path missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: false, + reason: "format", + message: "Malformed original ELF table.", + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + arguments: ["-e", "process.exit(0)"], + }); + }, + () => + Promise.resolve({ + path: root, + close: () => Promise.reject(new Error("root cleanup failed")), + }), + ); + expect(await provider.inspect({ path })).toMatchObject({ + ok: false, + error: { + cleanupIncomplete: true, + cleanupResources: [root], + diagnostics: { + reason: "root cleanup failed", + previous_error: { category: "invalid_input" }, + }, + }, + }); + }, +); +const fixture = async () => { + const workspace = await createTestWorkspace("rea-layout-boundary-"); + const root = workspace.root; + onTestFinished(() => removeTestWorkspace(root)); + const path = join(root, "selected.elf"); + await writeFile(path, "source-owned-seam-bytes"); + return { root, path }; +}; + +it.runIf(!unsupportedHost)( + "preserves cancellation during the actual signal-aware snapshot write and closes its acquired root", + async () => { + const { path } = await fixture(); + const controller = new AbortController(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + () => { + throw new Error("Decoder must not launch"); + }, + async () => { + const root = await PrivateRuntimeRoot.create({ + prefix: "rea-layout-cancel-", + }); + ownedPath = root.path; + controller.abort(); + return root; + }, + ); + expect( + await provider.inspect({ path }, { signal: controller.signal }), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it + .runIf(!unsupportedHost) + .each([ + "success", + "format", + "unavailable", + "unsupported", + "output-limit", + "wrong-profile", + "malformed-reply", + ])( + "preserves process/reply boundaries and cleans private snapshots: %s", + async (scenario) => { + const { root, path } = await fixture(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("request path missing"); + ownedPath = spawn.cwd ?? ""; + expect(spawn.arguments[0]).toBe("-I"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + expect(await readFile(request.snapshot_path, "utf8")).toBe( + "source-owned-seam-bytes", + ); + const { + artifact: _artifact, + diagnostics: _diagnostics, + ...value + } = binaryLayoutFixture(path); + await writeFile( + request.reply_path, + scenario === "malformed-reply" + ? "broken-json" + : JSON.stringify( + [ + "format", + "unavailable", + "unsupported", + "output-limit", + ].includes(scenario) + ? { + ok: false, + reason: scenario, + message: "ELF table is truncated.", + } + : { + ok: true, + profile: + scenario === "wrong-profile" + ? "pwntools@other" + : PWNTOOLS_PROVIDER_IDENTITY.version, + value, + }, + ), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", "process.stderr.write('upstream warning')"], + }); + }, + ); + const result = await provider.inspect({ path }); + if (scenario === "success") { + if (!result.ok) throw result.error; + expect(result.value.artifact).toMatchObject({ + path, + bytes: Buffer.byteLength("source-owned-seam-bytes"), + }); + expect(result.value.artifact.sha256).toMatch(/^[a-f0-9]{64}$/); + expect(result.value.diagnostics.stderr).toBe("upstream warning"); + } else + expect(result).toMatchObject({ + ok: false, + error: { + _tag: + scenario === "format" + ? "AnalysisInputError" + : scenario === "unavailable" + ? "ProviderSelectionError" + : scenario === "unsupported" + ? "AnalysisCapabilityUnavailableError" + : "AnalysisOutputError", + }, + }); + if (!result.ok) + expect(projectAnalysisError(result.error).details).toMatchObject({ + captured_output: { + stdout: "", + stderr: "upstream warning", + truncated: false, + }, + }); + expect(await readFile(path, "utf8")).toBe("source-owned-seam-bytes"); + expect(ownedPath.startsWith(root)).toBe(false); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost)( + "rejects missing configuration, absent input and prelaunch cancellation distinctly", + async () => { + const { path } = await fixture(); + expect( + await new PwntoolsLayoutProvider({}).inspect({ path }), + ).toMatchObject({ + ok: false, + error: { _tag: "ProviderSelectionError", reason: "provider_unavailable" }, + }); + const provider = new PwntoolsLayoutProvider({ + REA_PWNTOOLS_PYTHON: process.execPath, + }); + expect(await provider.inspect({ path: path + ".absent" })).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); + const controller = new AbortController(); + controller.abort(); + expect( + await provider.inspect({ path }, { signal: controller.signal }), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + }, +); +it.runIf(unsupportedHost)( + "reports unverified host coverage before acquiring a provider", + async () => { + expect( + await new PwntoolsLayoutProvider({}).inspect({ path: "/selected.elf" }), + ).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCapabilityUnavailableError" }, + }); + }, +); + +it.runIf(!unsupportedHost)( + "retains decoder warnings when cancellation arrives during successful root cleanup", + async () => { + const { path } = await fixture(); + const controller = new AbortController(); + let ownedPath = ""; + const provider = new PwntoolsLayoutProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("Request path missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + const { + artifact: _artifact, + diagnostics: _diagnostics, + ...value + } = binaryLayoutFixture(path); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: true, + profile: PWNTOOLS_PROVIDER_IDENTITY.version, + value, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", 'process.stderr.write("retained late warning")'], + }); + }, + async () => { + const root = await PrivateRuntimeRoot.create({ + prefix: "rea-layout-late-cancel-", + }); + ownedPath = root.path; + return { + path: root.path, + close: async () => { + await root.close(); + controller.abort(); + }, + }; + }, + ); + const result = await provider.inspect( + { path }, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + if (result.ok) throw new Error("Late cancellation must be preserved"); + expect(projectAnalysisError(result.error).details?.captured_output).toEqual( + { stdout: "", stderr: "retained late warning", truncated: false }, + ); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each(["directory", "missing-interpreter"])( + "identifies the configured executable on actual launch/preflight failure: %s", + async (scenario) => { + const { path, root } = await fixture(); + const executable = + scenario === "directory" ? root : join(root, "broken-python"); + if (scenario === "missing-interpreter") + await writeFile( + executable, + "#!/rea-missing-interpreter-for-boundary-test\n", + { mode: 0o700 }, + ); + const result = await new PwntoolsLayoutProvider({ + REA_PWNTOOLS_PYTHON: executable, + }).inspect({ path }); + if (result.ok) throw new Error("Expected unavailable executable"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "provider_unavailable", + details: { + rejections: [{ diagnostics: { executable_path: executable } }], + }, + }); + expect(result.error._tag).toBe("ProviderSelectionError"); + expect(await readFile(path, "utf8")).toBe("source-owned-seam-bytes"); + }, +); diff --git a/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts b/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts new file mode 100644 index 0000000000..5b3ca3b7bb --- /dev/null +++ b/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts @@ -0,0 +1,408 @@ +import type { RecordedCrash } from "../../../src/domain/native/recordedCrash.js"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; +import { + createTestWorkspace, + removeTestWorkspace, +} from "../../support/workspace/workspaceFixture.js"; +import { PwntoolsRecordedCrashProvider } from "../../../src/native/pwntools/PwntoolsRecordedCrashProvider.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; +import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; +import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; +import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; +import { + recordedCrashFixture, + recordedCrashDebuggerFixture, + recordedCrashFixtureBytes, + recordedCrashSignalFixture, +} from "../../fixtures/binaryDiagnostics/recordedCrash.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; +import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; + +const requestSchema = z.object({ + snapshot_path: z.string(), + reply_path: z.string(), +}); +const host = process.platform === "linux" && process.arch === "x64"; + +it + .runIf(host) + .each([ + "success", + "wrong-register", + "wrong-register-source", + "missing-thread", + "duplicate-thread", + "missing-signal", + "duplicate-signal", + "missing-register", + "duplicated-physical-thread", + "duplicated-physical-signal", + "omitted-physical-note", + "hidden-segment", + "wrong-header-range", + "wrong-note-kind", + "wrong-owner-display", + "wrong-pid", + "wrong-current-signal", + "wrong-signal-number", + "wrong-signal-code", + "wrong-signal-errno", + "wrong-signal-address", + "wrong-raw-note", + "malformed-range", + ] as const)( + "validates recorded source bytes and cleanup: %s", + async (scenario) => { + const { path, value, bytes } = await fixture( + true, + scenario === "wrong-owner-display", + ); + let owned = ""; + const provider = new PwntoolsRecordedCrashProvider( + { REA_PWNTOOLS_PYTHON: process.execPath }, + async (spawn) => { + owned = spawn.cwd ?? ""; + const request = requestSchema.parse( + JSON.parse(await readFile(spawn.arguments.at(-1) ?? "", "utf8")), + ); + expect(await readFile(request.snapshot_path)).toEqual(bytes); + const payload = structuredClone(value); + alterRecordedCrashReply(payload, scenario); + const { + artifact: _artifact, + diagnostics: _diagnostics, + debugger: _debugger, + decoder_diagnostics: _decoderDiagnostics, + ...raw + } = payload; + await writeFile( + request.reply_path, + JSON.stringify({ + ok: true, + profile: PWNTOOLS_PROVIDER_IDENTITY.version, + value: raw, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", "process.stdout.write('recorded warning')"], + }); + }, + ); + const result = await provider.inspect({ + path, + include_debugger_context: false, + }); + if (scenario === "success") { + if (!result.ok) throw result.error; + expect(result.value.threads).toEqual(value.threads); + expect(result.value.diagnostics.stdout).toBe("recorded warning"); + } else + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisOutputError", + capturedOutput: { stdout: "recorded warning" }, + }, + }); + expect(await readFile(path)).toEqual(bytes); + await expect(access(owned)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it + .runIf(host) + .each([ + "unconfigured", + "success", + "process-failure", + "invalid-context", + "cancelled", + "cleanup-failure", + ] as const)( + "owns optional context from the same snapshot: %s", + async (scenario) => { + const { path, value, bytes, root } = await fixture(); + const controller = new AbortController(); + let owned = ""; + let snapshotPath = ""; + const venv = join(root, "venv"); + const plugin = join(root, "gdbinit.py"); + await mkdir(venv); + await writeFile(plugin, ""); + const provider = new PwntoolsRecordedCrashProvider( + { + REA_PWNTOOLS_PYTHON: process.execPath, + ...(scenario === "unconfigured" + ? {} + : { + REA_PWNDBG_GDB: process.execPath, + REA_PWNDBG_GDBINIT: plugin, + REA_PWNDBG_VENV_PATH: venv, + }), + }, + async (spawn) => { + const request = requestSchema.parse( + JSON.parse(await readFile(spawn.arguments.at(-1) ?? "", "utf8")), + ); + expect(await readFile(request.snapshot_path)).toEqual(bytes); + if (snapshotPath === "") { + snapshotPath = request.snapshot_path; + owned = spawn.cwd ?? ""; + const { + artifact: _artifact, + diagnostics: _diagnostics, + debugger: _debugger, + decoder_diagnostics: _decoderDiagnostics, + ...raw + } = value; + await writeFile( + request.reply_path, + JSON.stringify({ + ok: true, + profile: PWNTOOLS_PROVIDER_IDENTITY.version, + value: raw, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", "process.stdout.write('basic stage diagnostic')"], + }); + } + expect(request.snapshot_path).toBe(snapshotPath); + const context = recordedCrashDebuggerFixture(); + if (scenario === "invalid-context") + context.maps = context.maps.map((map) => ({ ...map, end: "0x0" })); + await writeFile( + request.reply_path, + JSON.stringify({ ok: true, value: context }), + ); + const launched = await spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + scenario === "process-failure" + ? "process.stderr.write('debugger failed',()=>{process.exitCode=9;})" + : scenario === "cancelled" + ? "process.stdout.write('ready\\n'); setInterval(() => {}, 1000)" + : "process.stdout.write('debugger stage diagnostic')", + ], + }); + if (scenario === "cancelled") { + await waitForProviderProcessReady(launched.process); + setImmediate(() => controller.abort()); + } + if (scenario === "cleanup-failure") + return { + ...launched, + cleanup: async () => { + await cleanupOwnedProcessGroup(launched.ownership); + throw new Error("source-owned post-cleanup reporting failure"); + }, + }; + return launched; + }, + ); + const result = await provider.inspect( + { + path, + include_debugger_context: true, + }, + { signal: controller.signal }, + ); + if (scenario === "success") { + if (!result.ok) throw result.error; + expect(result.value.debugger).toMatchObject({ + status: "available", + maps: [{ permissions: null }], + diagnostics: { stdout: "debugger stage diagnostic" }, + }); + } else { + if (result.ok) throw new Error("Expected optional debugger failure"); + const projected = projectAnalysisError(result.error); + if (scenario === "unconfigured") + expect(projected).toMatchObject({ + details: { captured_output: { stdout: "basic stage diagnostic" } }, + }); + else if ( + scenario === "invalid-context" || + scenario === "cancelled" || + scenario === "cleanup-failure" + ) { + const serialized = JSON.stringify(projected); + expect(serialized).toContain("basic stage diagnostic"); + expect(serialized).toContain( + scenario === "cancelled" ? "ready" : "debugger stage diagnostic", + ); + expect(result.error._tag).toBe( + scenario === "invalid-context" + ? "AnalysisOutputError" + : scenario === "cancelled" + ? "AnalysisCancelledError" + : "ProviderAdapterError", + ); + if (scenario === "cleanup-failure") + expect(result.error).toBeInstanceOf(ProviderCleanupError); + } else + expect(projected).toMatchObject({ + category: "execution_failure", + details: { + diagnostics: { + preceding_output: { stdout: "basic stage diagnostic" }, + stderr: "debugger failed", + }, + }, + }); + } + expect(await readFile(path)).toEqual(bytes); + await expect(access(owned)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +const fixture = async (includeSignal = false, opaqueOwner = false) => { + const workspace = await createTestWorkspace("rea-recorded-core-seam-"); + onTestFinished(() => removeTestWorkspace(workspace.root)); + const path = join(workspace.root, "recorded.core"); + const value = includeSignal + ? recordedCrashSignalFixture(path) + : recordedCrashFixture(path); + if (opaqueOwner) { + value.notes = value.notes.map((note) => + note.index === 0 + ? { + ...note, + owner_bytes_base64: Buffer.from("EVIL\0").toString("base64"), + owner_display: "EVIL", + } + : note, + ); + value.threads = []; + } + const bytes = recordedCrashFixtureBytes(value); + await writeFile(path, bytes); + return { path, value, bytes, root: workspace.root }; +}; + +const alterRecordedCrashReply = ( + payload: RecordedCrash, + scenario: string, +): void => { + if (scenario === "wrong-owner-display") + payload.notes = payload.notes.map((note) => + note.index === 0 ? { ...note, owner_display: "FORGED" } : note, + ); + if ( + scenario === "duplicated-physical-thread" || + scenario === "duplicated-physical-signal" + ) { + const noteIndex = scenario === "duplicated-physical-thread" ? 0 : 1; + const note = payload.notes[noteIndex]; + if (note === undefined) throw new Error("missing fixture note"); + payload.notes.push({ ...note, index: 2 }); + if (noteIndex === 0) + payload.threads.push( + ...payload.threads.map((thread) => ({ + ...thread, + note_index: 2, + })), + ); + else + payload.signals.push( + ...payload.signals.map((signal) => ({ + ...signal, + note_index: 2, + })), + ); + } + if (scenario === "omitted-physical-note") { + payload.notes = payload.notes.filter((note) => note.index === 0); + payload.signals = []; + } + if (scenario === "hidden-segment") { + payload.segments = []; + payload.notes = []; + payload.threads = []; + payload.signals = []; + } + if (scenario === "wrong-header-range") + payload.segments = payload.segments.map((segment) => ({ + ...segment, + header_location: { offset: "0x0", bytes: "0x38" }, + })); + if (scenario === "wrong-note-kind") { + payload.notes = payload.notes.map((note) => + note.index === 0 ? { ...note, type: "NT_AUXV" } : note, + ); + payload.threads = []; + } + if (scenario === "wrong-register-source") + payload.threads = payload.threads.map((thread) => ({ + ...thread, + registers: thread.registers.map((register) => + register.name === "rdi" + ? { + ...register, + value: "0x0", + location: { offset: "0x104", bytes: "0x8" }, + } + : register, + ), + })); + if (scenario === "missing-thread") payload.threads = []; + if (scenario === "duplicate-thread") + payload.threads = [...payload.threads, ...payload.threads]; + if (scenario === "missing-signal") payload.signals = []; + if (scenario === "duplicate-signal") + payload.signals = [...payload.signals, ...payload.signals]; + if (scenario === "missing-register") + payload.threads = payload.threads.map((thread) => ({ + ...thread, + registers: thread.registers.slice(1), + })); + if (scenario === "wrong-register") + payload.threads = payload.threads.map((t) => ({ + ...t, + registers: t.registers.map((r) => ({ ...r, value: "0x1" })), + })); + if (scenario === "wrong-pid") + payload.threads = payload.threads.map((thread) => ({ + ...thread, + historical_pid: -2, + })); + if (scenario === "wrong-current-signal") + payload.threads = payload.threads.map((thread) => ({ + ...thread, + recorded_current_signal: -1, + })); + if (scenario.startsWith("wrong-signal")) + payload.signals = payload.signals.map((signal) => ({ + ...signal, + ...(scenario === "wrong-signal-number" + ? { + number: 10, + fault_address: null, + fault_address_meaning: "unknown" as const, + } + : scenario === "wrong-signal-code" + ? { code: 2 } + : scenario === "wrong-signal-errno" + ? { errno: 0 } + : { fault_address: "0x1" }), + })); + if (scenario === "wrong-raw-note") + payload.notes = payload.notes.map((n) => ({ + ...n, + descriptor_bytes_base64: Buffer.alloc(336, 1).toString("base64"), + })); + if (scenario === "malformed-range") + payload.notes = payload.notes.map((n) => ({ + ...n, + descriptor_location: { offset: "0x200", bytes: "0x150" }, + })); +}; diff --git a/tests/boundary/browser/browserObservationService.test.ts b/tests/boundary/browser/browserObservationService.test.ts deleted file mode 100644 index 9dd1c6328d..0000000000 --- a/tests/boundary/browser/browserObservationService.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { listBrowserTargetsInputSchema } from "../../../src/domain/browserObservation.js"; -import { browserCaptureComparisonInputSchema } from "../../../src/domain/browserCaptureComparison.js"; -import { compareWebScreenshotsInputSchema } from "../../../src/domain/webScreenshot.js"; -import { BROWSER_TOOL_CONTRACTS } from "../../../src/contracts/browserToolContracts.js"; -import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; -import { - compareWebCaptureEvidence, - compareWebScreenshotEvidence, - listBrowserTargets, -} from "../../../src/application/BrowserObservationService.js"; - -describe("browser observation service prerequisites", () => { - it("reports a missing provider without requiring a permission grant", async () => { - const endpoint = "http://127.0.0.1:9222"; - const origin = "http://[::1]:3000"; - const result = await listBrowserTargets( - undefined, - listBrowserTargetsInputSchema.parse({ - cdp_endpoint: endpoint, - allowed_origins: [origin], - }), - ); - expect(result).toMatchObject({ - ok: false, - error: { - _tag: "AnalysisCapabilityUnavailableError", - }, - }); - }); - it("compares supplied scenarios and PNG artifacts without a CDP endpoint", async () => { - const browser = new CdpBrowserProvider(); - const captures = await compareWebCaptureEvidence( - browser, - browserCaptureComparisonInputSchema.parse( - example("compare_web_captures"), - ), - ); - const screenshots = await compareWebScreenshotEvidence( - browser, - compareWebScreenshotsInputSchema.parse( - example("compare_web_screenshots"), - ), - ); - for (const result of [captures, screenshots]) { - expect(result.ok).toBe(true); - if (!result.ok) throw result.error; - expect(result.value.parameters).not.toHaveProperty("cdp_endpoint"); - } - }); -}); - -const example = (name: string) => { - const input = BROWSER_TOOL_CONTRACTS.find((tool) => tool.name === name) - ?.examples[0]?.input; - if (input === undefined) throw new Error(`Missing example for ${name}`); - return input; -}; diff --git a/tests/boundary/browser/cdpBrowserProvider.discovery_authorization.test.ts b/tests/boundary/browser/cdpBrowserProvider.discovery_authorization.test.ts index d65e150b4e..053cd673ea 100644 --- a/tests/boundary/browser/cdpBrowserProvider.discovery_authorization.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.discovery_authorization.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { @@ -6,9 +6,9 @@ import { listBrowserTargetsInputSchema, } from "../../../src/domain/browserObservation.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; -import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js"; +import { trackBrowser } from "./cdpBrowserProvider.support.js"; -describeBrowser("CdpBrowserProvider: discovery authorization 1", () => { +describe("CdpBrowserProvider: discovery authorization 1", () => { it("discovers pages and captures a selected target without an origin list", async () => { const browser = await startFakeCdpBrowser(); trackBrowser(browser); @@ -73,9 +73,12 @@ describeBrowser("CdpBrowserProvider: discovery authorization 1", () => { const browser = await startFakeCdpBrowser({ urlShapedAllowedTitle }); trackBrowser(browser); const provider = new CdpBrowserProvider(); - const input = { + const listInput = { cdp_endpoint: browser.endpoint, allowed_origins: [browser.allowedOrigin], + }; + const input = { + ...listInput, target_id: "allowed-page", }; const authority = browser.allowedOrigin.replace(/^https?:\/\//u, ""); @@ -86,7 +89,7 @@ describeBrowser("CdpBrowserProvider: discovery authorization 1", () => { ? "/app?startup=title-secret#fragment" : `${prefix}${authority}/app?startup=title-secret#fragment`; const listed = await provider.listTargets( - listBrowserTargetsInputSchema.parse(input), + listBrowserTargetsInputSchema.parse(listInput), ); if (!listed.ok) throw listed.error; expect(listed.value.targets[0]?.title).toBe(expected); @@ -155,7 +158,7 @@ describeBrowser("CdpBrowserProvider: discovery authorization 1", () => { }); }); -describeBrowser("CdpBrowserProvider: discovery authorization 2", () => { +describe("CdpBrowserProvider: discovery authorization 2", () => { it("rejects empty browser attachment session identifiers", async () => { const browser = await startFakeCdpBrowser({ invalidAttachedSession: true }); trackBrowser(browser); diff --git a/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts b/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts index e770000b17..6911860494 100644 --- a/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js"; @@ -6,20 +6,9 @@ import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalys import { discoverWebMcpToolsInputSchema } from "../../../src/domain/webMcpDiscovery.js"; import { captureWebScreenshotInputSchema } from "../../../src/domain/webScreenshot.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; -import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js"; - -describeBrowser("CdpBrowserProvider: document script 1", () => { - it("rejects unknown WebMCP discovery fields", () => { - expect( - discoverWebMcpToolsInputSchema.safeParse({ - cdp_endpoint: "http://127.0.0.1:9222", - allowed_origins: ["https://example.test"], - target_id: "target", - unknown_field: true, - }).success, - ).toBe(false); - }); +import { trackBrowser } from "./cdpBrowserProvider.support.js"; +describe("CdpBrowserProvider: approved accessibility and source capture", () => { it("captures approved accessibility text inline", async () => { const browser = await startFakeCdpBrowser(); trackBrowser(browser); @@ -120,7 +109,7 @@ describeBrowser("CdpBrowserProvider: document script 1", () => { }); }); -describeBrowser("CdpBrowserProvider: transient WebMCP frames", () => { +describe("CdpBrowserProvider: transient WebMCP frames", () => { it("drops stale child tools and reports partial coverage when a blank commit remains", async () => { const browser = await startFakeCdpBrowser({ webMcpTools: true, @@ -205,7 +194,7 @@ describeBrowser("CdpBrowserProvider: transient WebMCP frames", () => { }); }); -describeBrowser("CdpBrowserProvider: document script 2", () => { +describe("CdpBrowserProvider: WebMCP discovery and completeness", () => { it("discovers untrusted WebMCP declarations without registering or invoking them", async () => { const browser = await startFakeCdpBrowser({ webMcpTools: true }); trackBrowser(browser); @@ -360,7 +349,7 @@ describeBrowser("CdpBrowserProvider: document script 2", () => { }); }); -describeBrowser("CdpBrowserProvider WebMCP inventory completeness", () => { +describe("CdpBrowserProvider WebMCP inventory completeness", () => { it("retains every field in a large declared WebMCP input schema", async () => { const propertyCount = 5_001; const browser = await startFakeCdpBrowser({ @@ -392,7 +381,7 @@ describeBrowser("CdpBrowserProvider WebMCP inventory completeness", () => { }); }); -describeBrowser("CdpBrowserProvider: document script 3", () => { +describe("CdpBrowserProvider: frame scope and screenshots", () => { it("removes WebMCP declarations after their child frame leaves scope", async () => { const browser = await startFakeCdpBrowser({ webMcpTools: true, @@ -474,7 +463,7 @@ describeBrowser("CdpBrowserProvider: document script 3", () => { }); }); -describeBrowser("CdpBrowserProvider: document script 4", () => { +describe("CdpBrowserProvider: complete passive inventories", () => { it("returns every frame, resource, worker, accessibility, and storage inventory item", async () => { const browser = await startFakeCdpBrowser({ extraCollections: true }); trackBrowser(browser); @@ -501,7 +490,7 @@ describeBrowser("CdpBrowserProvider: document script 4", () => { }); }); -describeBrowser("CdpBrowserProvider: WebMCP registration owners", () => { +describe("CdpBrowserProvider: WebMCP registration owners", () => { it.each(["retain", "remove-second"] as const)( "keeps same-URL frame owners separate: %s", async (mode) => { diff --git a/tests/boundary/browser/cdpBrowserProvider.lifecycle_failures.test.ts b/tests/boundary/browser/cdpBrowserProvider.lifecycle_failures.test.ts index d559b90887..3d5fc7a6f8 100644 --- a/tests/boundary/browser/cdpBrowserProvider.lifecycle_failures.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.lifecycle_failures.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { @@ -11,9 +11,9 @@ import { startFakeCdpBrowser, type FakeCdpBrowser, } from "../../fixtures/fakeCdpBrowser.js"; -import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js"; +import { trackBrowser } from "./cdpBrowserProvider.support.js"; -describeBrowser("CdpBrowserProvider: lifecycle failures 1", () => { +describe("CdpBrowserProvider: cancellation and session events", () => { it("projects direct-session cancellation onto the requested operation", async () => { const browser = await startFakeCdpBrowser({ pageScopedVersionWebSocket: true, @@ -132,7 +132,7 @@ describeBrowser("CdpBrowserProvider: lifecycle failures 1", () => { }); }); -describeBrowser("CdpBrowserProvider: lifecycle failures 2", () => { +describe("CdpBrowserProvider: session termination and scope changes", () => { it("ends a browser session when its flat target session detaches", async () => { const browser = await startFakeCdpBrowser({ sessionTimeline: "target_detached", @@ -243,7 +243,7 @@ describeBrowser("CdpBrowserProvider: lifecycle failures 2", () => { }); }); -describeBrowser("CdpBrowserProvider: lifecycle failures 3", () => { +describe("CdpBrowserProvider: protocol failures and cancellation cleanup", () => { it("degrades optional domains but propagates protocol and payload failures", async () => { const optional = await startFakeCdpBrowser({ unsupportedMethods: ["Accessibility.getFullAXTree"], @@ -373,7 +373,7 @@ describeBrowser("CdpBrowserProvider: lifecycle failures 3", () => { }); }); -describeBrowser("CdpBrowserProvider: lifecycle failures 4", () => { +describe("CdpBrowserProvider: navigation commit and target identity", () => { it("waits for navigation commit and rechecks the attached main-frame origin", async () => { const transitioning = await startFakeCdpBrowser({ transitionalFrameReads: 2, diff --git a/tests/boundary/browser/cdpBrowserProvider.network.test.ts b/tests/boundary/browser/cdpBrowserProvider.network.test.ts index 4787435d3f..152b2ef14b 100644 --- a/tests/boundary/browser/cdpBrowserProvider.network.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.network.test.ts @@ -1,11 +1,64 @@ -import { expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; -import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js"; +import { trackBrowser } from "./cdpBrowserProvider.support.js"; + +describe("CdpBrowserProvider: network 1", () => { + it("retains same-origin redirect hops on the one final request", async () => { + const browser = await startFakeCdpBrowser({ redirectWithinOrigin: true }); + trackBrowser(browser); + const result = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + + if (!result.ok) throw result.error; + expect(result.value.network.requests).toHaveLength(1); + expect(result.value.network.requests[0]).toMatchObject({ + request_id: "request-1", + url: `${browser.allowedOrigin}/redirected?token=final-secret`, + status: 200, + redirects: [ + { + url: `${browser.allowedOrigin}/api?token=network-secret`, + response_url: `${browser.allowedOrigin}/api?token=network-secret`, + method: "POST", + resource_type: "Fetch", + status: 301, + mime_type: "text/plain", + encoded_data_length: 23, + request_timestamp: 7, + redirect_event_timestamp: 8, + }, + { + url: `${browser.allowedOrigin}/intermediate`, + response_url: `${browser.allowedOrigin}/intermediate`, + method: "GET", + resource_type: "Fetch", + status: 302, + mime_type: "text/plain", + encoded_data_length: 27, + request_timestamp: 8, + redirect_event_timestamp: 9, + }, + ], + }); + expect(JSON.stringify(result.value.network.requests)).not.toContain( + "redirect-header-secret", + ); + expect(result.value.completeness.excluded).not.toContainEqual({ + section: "network_requests", + reason: "invalid_protocol_value", + count: expect.any(Number), + }); + }); -describeBrowser("CdpBrowserProvider: network 1", () => { it("drops network evidence when a request redirects outside the approved origin", async () => { const browser = await startFakeCdpBrowser({ redirectToDisallowedOrigin: true, @@ -23,4 +76,42 @@ describeBrowser("CdpBrowserProvider: network 1", () => { if (!result.ok) throw result.error; expect(result.value.network.requests).toEqual([]); }); + + it("does not retain excluded prior-hop data when a redirect returns in scope", async () => { + const browser = await startFakeCdpBrowser({ + redirectFromDisallowedOrigin: true, + }); + trackBrowser(browser); + const result = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + + if (!result.ok) throw result.error; + expect(result.value.network.requests).toHaveLength(1); + expect(result.value.network.requests[0]).toMatchObject({ + url: `${browser.allowedOrigin}/returned`, + redirects: [], + }); + const output = JSON.stringify(result.value.network.requests); + expect(output).not.toContain("private.example.test"); + expect(output).not.toContain("redirect-header-secret"); + expect(output).not.toContain("redirect-body-secret"); + expect(result.value.completeness.policy_filtered_sections).toContain( + "network_requests", + ); + expect(result.value.completeness.status).toBe("policy_filtered"); + expect(result.value.completeness.unavailable_sections).not.toContain( + "network_requests", + ); + expect(result.value.completeness.excluded).not.toContainEqual({ + section: "network_requests", + reason: "invalid_protocol_value", + count: expect.any(Number), + }); + }); }); diff --git a/tests/boundary/browser/cdpBrowserProvider.redirectErrors.test.ts b/tests/boundary/browser/cdpBrowserProvider.redirectErrors.test.ts new file mode 100644 index 0000000000..acfbe4b070 --- /dev/null +++ b/tests/boundary/browser/cdpBrowserProvider.redirectErrors.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest"; + +import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; +import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js"; +import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; +import type { FakeOptions } from "../../fixtures/fakeCdpBrowserTypes.js"; +import { trackBrowser } from "./cdpBrowserProvider.support.js"; + +const inspectNetwork = async (options: FakeOptions) => { + const browser = await startFakeCdpBrowser(options); + trackBrowser(browser); + const result = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + include_json_body_shapes: true, + }), + ); + if (!result.ok) throw result.error; + return { browser, inspection: result.value }; +}; + +describe("CdpBrowserProvider: redirect errors", () => { + it.each([ + { label: "missing", url: undefined, reason: "invalid_protocol_value" }, + { label: "malformed", url: "http://%", reason: "invalid_protocol_value" }, + { + label: "unsupported scheme", + url: "file:///private/redirect", + reason: "unsupported_url", + }, + ])("classifies $label redirect response URLs", async ({ url, reason }) => { + const { inspection } = await inspectNetwork({ + malformedRedirectResponse: true, + ...(url === undefined ? {} : { redirectResponseUrl: url }), + }); + + expect(inspection.network.requests).toEqual([]); + expect(inspection.completeness.excluded).toContainEqual({ + section: "network_requests", + reason, + count: expect.any(Number), + }); + const section = + reason === "unsupported_url" + ? inspection.completeness.policy_filtered_sections + : inspection.completeness.unavailable_sections; + expect(section).toContain("network_requests"); + }); + + it.each([ + { label: "null", envelope: null }, + { label: "array", envelope: [] }, + { label: "scalar", envelope: "malformed redirect" }, + ])( + "preserves prior request for a $label redirect envelope", + async ({ envelope }) => { + const { browser, inspection } = await inspectNetwork({ + malformedRedirectResponse: true, + redirectResponseEnvelope: envelope, + }); + + expect(inspection.network.requests).toHaveLength(1); + expect(inspection.network.requests[0]).toMatchObject({ + request_id: "request-1", + url: `${browser.allowedOrigin}/malformed-redirect-prior`, + status: 201, + mime_type: "application/json", + encoded_data_length: null, + redirects: [ + { + url: `${browser.allowedOrigin}/api?token=network-secret`, + response_url: `${browser.allowedOrigin}/api?token=network-secret`, + request_timestamp: 7, + redirect_event_timestamp: 8, + }, + ], + }); + expect( + inspection.network.requests[0]?.body_shapes.request, + ).not.toBeNull(); + expect(inspection.network.requests[0]?.body_shapes.response).toBeNull(); + expect(inspection.network.requests[0]?.body_shapes.status).toBe( + "partial", + ); + expect(inspection.metadata.responses).toHaveLength(1); + expect(inspection.metadata.responses[0]).toMatchObject({ + url: `${browser.allowedOrigin}/malformed-redirect-prior`, + mime_type: "application/json", + content_length: 123, + content_encoding: "gzip", + }); + expect(inspection.completeness.unavailable_sections).toContain( + "network_requests", + ); + expect(inspection.completeness.unavailable_sections).toContain( + "json_body_shapes", + ); + expect(inspection.completeness.excluded).toContainEqual({ + section: "network_requests", + reason: "invalid_protocol_value", + count: expect.any(Number), + }); + expect(inspection.completeness.excluded).toContainEqual({ + section: "json_body_shapes", + reason: "invalid_protocol_value", + count: expect.any(Number), + }); + expect(JSON.stringify(inspection.network.requests)).not.toContain( + "malformed-redirect-final", + ); + }, + ); + + it("keeps authorized prior evidence when the later response URL is disallowed", async () => { + const { browser, inspection } = await inspectNetwork({ + malformedRedirectResponse: true, + redirectResponseEnvelope: null, + responseAfterMalformedUrl: + "https://private.example.test/late?token=unauthorized-secret", + }); + + expect(inspection.network.requests).toHaveLength(1); + expect(inspection.network.requests[0]).toMatchObject({ + url: `${browser.allowedOrigin}/malformed-redirect-prior`, + status: 201, + mime_type: "application/json", + encoded_data_length: null, + body_shapes: { status: "partial", response: null }, + }); + expect(inspection.metadata.responses).toHaveLength(1); + expect(inspection.metadata.responses[0]?.url).toBe( + `${browser.allowedOrigin}/malformed-redirect-prior`, + ); + expect(inspection.completeness.unavailable_sections).toContain( + "json_body_shapes", + ); + expect(inspection.completeness.excluded).toContainEqual({ + section: "network_requests", + reason: "invalid_protocol_value", + count: expect.any(Number), + }); + const output = JSON.stringify(inspection); + expect(output).not.toContain("private.example.test"); + expect(output).not.toContain("unauthorized-secret"); + expect(output).not.toContain("header-secret"); + }); +}); diff --git a/tests/boundary/browser/cdpBrowserProvider.sensitive_data.test.ts b/tests/boundary/browser/cdpBrowserProvider.sensitive_data.test.ts index 20bcabaac2..d477643814 100644 --- a/tests/boundary/browser/cdpBrowserProvider.sensitive_data.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.sensitive_data.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { @@ -9,9 +9,9 @@ import { startFakeCdpBrowser, type FakeCdpBrowser, } from "../../fixtures/fakeCdpBrowser.js"; -import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js"; +import { trackBrowser } from "./cdpBrowserProvider.support.js"; -describeBrowser("CdpBrowserProvider: sensitive data 1", () => { +describe("CdpBrowserProvider: passive evidence redaction", () => { it("captures bounded passive evidence without retaining sensitive values", async () => { const browser = await startFakeCdpBrowser({ binaryWebSocketEvent: true, @@ -158,7 +158,7 @@ describeBrowser("CdpBrowserProvider: sensitive data 1", () => { }); }); -describeBrowser("CdpBrowserProvider: page cookie scope", () => { +describe("CdpBrowserProvider: page cookie scope", () => { it("uses the attached page URL for cookies and its origin for DOM storage", async () => { const browser = await startFakeCdpBrowser(); trackBrowser(browser); @@ -190,7 +190,7 @@ describeBrowser("CdpBrowserProvider: page cookie scope", () => { }); }); -describeBrowser("CdpBrowserProvider: complete storage fingerprints", () => { +describe("CdpBrowserProvider: complete storage fingerprints", () => { it("fingerprints cache bodies above the former byte ceiling", async () => { const body = "x".repeat(64 * 1_024 + 1); const browser = await startFakeCdpBrowser({ cachedResponseBody: body }); @@ -217,7 +217,7 @@ describeBrowser("CdpBrowserProvider: complete storage fingerprints", () => { }); }); -describeBrowser("CdpBrowserProvider: sensitive data 2", () => { +describe("CdpBrowserProvider: explicitly requested text and payload shapes", () => { it("captures requested console text verbatim and value-free payload shapes", async () => { const browser = await startFakeCdpBrowser({ sensitiveShapes: true, @@ -309,45 +309,7 @@ describeBrowser("CdpBrowserProvider: sensitive data 2", () => { }); }); -describeBrowser("CdpBrowserProvider: complete sensitive data", () => { - it("returns complete requested text and payload shapes verbatim", async () => { - const browser = await startFakeCdpBrowser({ sensitiveShapes: true }); - trackBrowser(browser); - const request = inspectWebPageInputSchema.parse({ - cdp_endpoint: browser.endpoint, - allowed_origins: [browser.allowedOrigin], - target_id: "allowed-page", - observation_ms: 0, - include_console_text: true, - include_json_body_shapes: true, - include_websocket_shapes: true, - }); - const result = await new CdpBrowserProvider().inspectPage(request); - - if (!result.ok) throw result.error; - expect(result.value.console.events[0]?.text_capture).toEqual({ - status: "included", - values: [ - { - argument_index: 0, - type: "string", - text: "authorization=Bearer console-secret", - }, - { argument_index: 1, type: "number", text: "42" }, - ], - retained_bytes: - Buffer.byteLength("authorization=Bearer console-secret") + - Buffer.byteLength("42"), - }); - expect(result.value.network.requests[0]?.body_shapes.status).toBe( - "included", - ); - expect(result.value.network.websocket_events[0]?.payload_shape).toEqual({ - format: "json", - json_shape: expect.objectContaining({ root_type: "object" }), - }); - }); - +describe("CdpBrowserProvider: malformed approved payloads", () => { it("fails closed on malformed approved response and binary payload encodings", async () => { const browser = await startFakeCdpBrowser({ invalidResponseBodyBase64: true, diff --git a/tests/boundary/browser/cdpBrowserProvider.support.ts b/tests/boundary/browser/cdpBrowserProvider.support.ts index 1afe9e0747..8e59d2d031 100644 --- a/tests/boundary/browser/cdpBrowserProvider.support.ts +++ b/tests/boundary/browser/cdpBrowserProvider.support.ts @@ -1,4 +1,4 @@ -import { afterEach, describe } from "vitest"; +import { afterEach } from "vitest"; import type { FakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; @@ -11,5 +11,3 @@ afterEach(async () => { export const trackBrowser = (browser: FakeCdpBrowser): void => { browsers.push(browser); }; - -export const describeBrowser = describe; diff --git a/tests/boundary/browser/cdpBrowserProvider.timelineEvents.test.ts b/tests/boundary/browser/cdpBrowserProvider.timelineEvents.test.ts index c77af5985a..e9f1f9a60f 100644 --- a/tests/boundary/browser/cdpBrowserProvider.timelineEvents.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.timelineEvents.test.ts @@ -1,29 +1,11 @@ -import { expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; -import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js"; - -describeBrowser("CdpBrowserProvider navigation timeline", () => { - it("accepts caller-selected windows and rejects the removed event-count option", () => { - const accepted = observeWebSessionInputSchema.safeParse({ - cdp_endpoint: "http://127.0.0.1:9222", - allowed_origins: ["https://app.example.test"], - target_id: "page-1", - observation_ms: 120_000, - }); - expect(accepted.success).toBe(true); - expect( - observeWebSessionInputSchema.safeParse({ - cdp_endpoint: "http://127.0.0.1:9222", - allowed_origins: ["https://app.example.test"], - target_id: "page-1", - max_timeline_events: 1, - }).success, - ).toBe(false); - }); +import { trackBrowser } from "./cdpBrowserProvider.support.js"; +describe("CdpBrowserProvider navigation timeline", () => { it("returns every event beyond the former default collection cap", async () => { const additionalEvents = 2_001; const browser = await startFakeCdpBrowser({ diff --git a/tests/boundary/browser/cdpCaptureDocuments.test.ts b/tests/boundary/browser/cdpCaptureDocuments.test.ts index 92612d9be9..9a11c45102 100644 --- a/tests/boundary/browser/cdpCaptureDocuments.test.ts +++ b/tests/boundary/browser/cdpCaptureDocuments.test.ts @@ -1,52 +1,61 @@ import { expect, it } from "vitest"; -import { CdpCaptureCompleteness } from "../../../src/browser/CdpCaptureCompleteness.js"; -import { - captureFrames, - captureResources, -} from "../../../src/browser/CdpCaptureDocuments.js"; +import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; +import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js"; +import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; +import { trackBrowser } from "./cdpBrowserProvider.support.js"; -it("reports malformed child frame entries instead of silently completing inventory", () => { - const completeness = new CdpCaptureCompleteness(); - const captured = captureFrames( - { - frameTree: { - frame: { - id: "main", - url: "https://example.test/", - }, - childFrames: [ - null, - { frame: { id: "child", url: "https://example.test/child" } }, - ], - }, - }, - new Set(["https://example.test"]), - undefined, - completeness, +it("reports malformed child frame entries through the CDP boundary", async () => { + const browser = await startFakeCdpBrowser({ + commandResult: (command, origin) => + command.method === "Page.getFrameTree" + ? { + frameTree: { + frame: { id: "main", url: `${origin}/` }, + childFrames: [ + null, + { frame: { id: "child", url: `${origin}/child` } }, + ], + }, + } + : undefined, + }); + trackBrowser(browser); + + const result = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), ); - expect(captured.items.map(({ frame_id }) => frame_id)).toEqual([ + if (!result.ok) throw result.error; + expect(result.value.frames.map(({ frame_id }) => frame_id)).toEqual([ "main", "child", ]); - expect(completeness.snapshot().unavailable_sections).toContain("frames"); + expect(result.value.completeness.unavailable_sections).toContain("frames"); }); -it("reports missing top-level frame trees for frame and resource inventories", () => { - const completeness = new CdpCaptureCompleteness(); - expect( - captureFrames( - {}, - new Set(["https://example.test"]), - undefined, - completeness, - ).items, - ).toEqual([]); - expect( - captureResources({}, new Set(["https://example.test"]), completeness).items, - ).toEqual([]); - expect(completeness.snapshot().unavailable_sections).toEqual( - expect.arrayContaining(["frames", "resources"]), +it("marks a missing resource frame tree incomplete through CDP", async () => { + const browser = await startFakeCdpBrowser({ + commandResult: (command) => + command.method === "Page.getResourceTree" ? {} : undefined, + }); + trackBrowser(browser); + + const result = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), ); + + if (!result.ok) throw result.error; + expect(result.value.resources).toEqual([]); + expect(result.value.completeness.unavailable_sections).toContain("resources"); }); diff --git a/tests/boundary/browser/cdpCaptureNavigation.test.ts b/tests/boundary/browser/cdpCaptureNavigation.test.ts index 070d6b1122..4716076406 100644 --- a/tests/boundary/browser/cdpCaptureNavigation.test.ts +++ b/tests/boundary/browser/cdpCaptureNavigation.test.ts @@ -1,65 +1,8 @@ import { expect, it, onTestFinished } from "vitest"; -import { CdpCaptureEvents } from "../../../src/browser/CdpCaptureEvents.js"; import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; -import type { CdpEvent } from "../../../src/browser/CdpConnection.js"; - -const events = (): CdpCaptureEvents => - new CdpCaptureEvents( - inspectWebPageInputSchema.parse({ - cdp_endpoint: "http://127.0.0.1:9222", - allowed_origins: ["https://example.test"], - target_id: "page-1", - }), - new Set(["https://example.test"]), - ); - -const frameNavigated = (url: string, loaderId: string): CdpEvent => ({ - method: "Page.frameNavigated", - params: { - frame: { id: "main", url, loaderId }, - }, -}); - -it("ignores a repeated frameNavigated for the document already committed", () => { - const state = events(); - state.beginAuthorizedFrame("main"); - state.ingest(frameNavigated("https://example.test/page", "loader-1")); - expect(state.navigationDuringCapture).toBe(true); - state.navigationDuringCapture = false; - - // A re-fire with the same URL and loader is not a navigation. - state.ingest(frameNavigated("https://example.test/page", "loader-1")); - expect(state.navigationDuringCapture).toBe(false); -}); - -it("still reports a genuine change of document", () => { - const state = events(); - state.beginAuthorizedFrame("main"); - state.ingest(frameNavigated("https://example.test/page", "loader-1")); - state.navigationDuringCapture = false; - state.ingest(frameNavigated("https://example.test/other", "loader-2")); - expect(state.navigationDuringCapture).toBe(true); -}); - -it("does not forget a navigation that happened during the observation window", () => { - const state = events(); - state.beginAuthorizedFrame("main"); - state.ingest(frameNavigated("https://example.test/page", "loader-1")); - // The final capture re-attaches to the resolved frame; clearing the flag here - // used to report a clean capture while carrying pre-navigation state. - state.beginFinalCapture("main"); - expect(state.navigationDuringCapture).toBe(true); -}); - -it("keeps a settled observation window clean", () => { - const state = events(); - state.beginAuthorizedFrame("main"); - state.beginFinalCapture("main"); - expect(state.navigationDuringCapture).toBe(false); -}); it("captures a page without aborting on a duplicate frame event", async () => { const browser = await startFakeCdpBrowser({ sessionTimeline: "same_origin" }); diff --git a/tests/boundary/browser/cdpElectronProvider.test.ts b/tests/boundary/browser/cdpElectronProvider.test.ts index b15ffaa439..3a8cdffdb4 100644 --- a/tests/boundary/browser/cdpElectronProvider.test.ts +++ b/tests/boundary/browser/cdpElectronProvider.test.ts @@ -199,25 +199,6 @@ describe("CdpElectronProvider target selection", () => { }); }); -describe("Electron request schemas", () => { - it("rejects unknown request fields", () => { - expect( - listElectronTargetsInputSchema.safeParse({ - cdp_endpoint: "http://127.0.0.1:9223", - unknown_field: true, - }).success, - ).toBe(false); - expect( - inspectElectronPageInputSchema.safeParse({ - cdp_endpoint: "http://127.0.0.1:9223", - target_id: "page-1", - observation_ms: 0, - unknown_field: true, - }).success, - ).toBe(false); - }); -}); - describe("CdpElectronProvider capture", () => { it("captures Electron script source only after separate approval", async () => { const root = await electronFixture(); diff --git a/tests/boundary/browser/cdpWebRuntimeSourceIdentity.test.ts b/tests/boundary/browser/cdpWebRuntimeSourceIdentity.test.ts index 419b8fb31b..e14b81d207 100644 --- a/tests/boundary/browser/cdpWebRuntimeSourceIdentity.test.ts +++ b/tests/boundary/browser/cdpWebRuntimeSourceIdentity.test.ts @@ -82,7 +82,12 @@ it.each( }); } else { const result = await provider.observeExecution( - observeWebExecutionInputSchema.parse({ ...scope, observation_ms: 5 }), + observeWebExecutionInputSchema.parse({ + ...scope, + // This fixture sends a real WebSocket event after the armed receipt. + // Leave room for transport delivery on a shared CI runner. + observation_ms: 100, + }), { progress: { report: async (update) => { @@ -124,8 +129,17 @@ it.each( ({ script_id }) => script_id === "script-a", ), ).toBe(false); - expect(result.value.requests[0]?.callsites[0]?.source_association).toBe( - "unknown", + expect(result.value.window.end_reason).toBe("window_elapsed"); + expect(result.value.requests).toContainEqual( + expect.objectContaining({ + request_id: "source-completion-request", + callsites: expect.arrayContaining([ + expect.objectContaining({ + script_id: "script-a", + source_association: "unknown", + }), + ]), + }), ); expect(result.value.script_inventory.main_document_scripts).toBe(1); expect( diff --git a/tests/boundary/browser/electronActiveHook.test.ts b/tests/boundary/browser/electronActiveHook.test.ts index 75ad44f839..2afaebc7ad 100644 --- a/tests/boundary/browser/electronActiveHook.test.ts +++ b/tests/boundary/browser/electronActiveHook.test.ts @@ -122,8 +122,11 @@ it("retains active hook lifecycle and IPC evidence without a count ceiling", asy const script = join(root, "exercise.cjs"); await writeFile(script, activeCaptureSource); const hook = join(process.cwd(), "scripts/electron-active-hook.cjs"); - const output = await runNode(hook, script, root, root); - const result = JSON.parse(output.trim().split("\n").at(-1) ?? "null") as { + const execution = await runNode(hook, script, root, root); + expect(execution.code).toBe(0); + const result = JSON.parse( + execution.stdout.trim().split("\n").at(-1) ?? "null", + ) as { readonly default_popup_decision: { readonly action: string }; readonly popup_decision: { readonly action: string }; readonly snapshot: { @@ -181,7 +184,7 @@ it("preserves fatal process termination after recording uncaught exceptions", as const script = join(root, "fatal.cjs"); await writeFile(script, 'throw new Error("fatal-hook-secret");\n'); const hook = join(process.cwd(), "scripts/electron-active-hook.cjs"); - const result = await runNodeStatus(hook, script, root, root); + const result = await runNode(hook, script, root, root); expect(result.code).not.toBe(0); expect(result.stderr).toContain("fatal-hook-secret"); @@ -192,7 +195,11 @@ const runNode = ( script: string, moduleRoot: string, cwd: string, -): Promise => +): Promise<{ + readonly code: number | null; + readonly stdout: string; + readonly stderr: string; +}> => new Promise((resolve, reject) => { const child = spawn(process.execPath, ["-r", hook, script], { cwd, @@ -207,44 +214,11 @@ const runNode = ( child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk)); child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk)); child.once("error", reject); - child.once("close", (code) => { - if (code === 0) { - const text = Buffer.concat(stdout).toString("utf8"); - if (text.length > 0) resolve(text); - else - reject( - new Error( - `hook fixture produced no output: ${Buffer.concat(stderr).toString("utf8")}`, - ), - ); - } else - reject( - new Error( - `hook fixture exited with ${String(code)}: ${Buffer.concat(stderr).toString("utf8")}`, - ), - ); - }); - }); - -const runNodeStatus = ( - hook: string, - script: string, - moduleRoot: string, - cwd: string, -): Promise<{ readonly code: number | null; readonly stderr: string }> => - new Promise((resolve, reject) => { - const child = spawn(process.execPath, ["-r", hook, script], { - cwd, - env: { - ...process.env, - NODE_PATH: moduleRoot, - }, - stdio: ["ignore", "pipe", "pipe"], - }); - const stderr: Buffer[] = []; - child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk)); - child.once("error", reject); child.once("close", (code) => - resolve({ code, stderr: Buffer.concat(stderr).toString("utf8") }), + resolve({ + code, + stdout: Buffer.concat(stdout).toString("utf8"), + stderr: Buffer.concat(stderr).toString("utf8"), + }), ); }); diff --git a/tests/boundary/browser/historicalCaptureDecoder.test.ts b/tests/boundary/browser/historicalCaptureDecoder.test.ts new file mode 100644 index 0000000000..5187d1f6a7 --- /dev/null +++ b/tests/boundary/browser/historicalCaptureDecoder.test.ts @@ -0,0 +1,481 @@ +import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; +import { + access, + chmod, + mkdtemp, + rm, + truncate, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, it, onTestFinished } from "vitest"; +import { HistoricalCaptureDecoder } from "../../../src/browser/history/HistoricalCaptureDecoder.js"; +import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../../src/browser/history/CaptureRelease.js"; +import { + inspectWebNetworkCaptureInputSchema, + WEB_NETWORK_CAPTURE_LIMITS, +} from "../../../src/domain/webNetworkCapture.js"; +import { historicalHar } from "../../fixtures/historicalHar.js"; +import { decodeHarCapture } from "../../../src/browser/history/HarCapture.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; + +it.each([true, false])( + "binds decoder replies to the complete selected provider identity (matching name: %s)", + async (matching) => { + const root = await mkdtemp(join(tmpdir(), "rea-capture-identity-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "capture.har"); + const text = JSON.stringify(historicalHar()); + await writeFile(path, text); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async (_, runtimePath) => { + const value = decodeHarCapture(text, []); + if (!matching) value.decoder.name = "different adapter"; + await writeFile( + join(runtimePath, "reply.json"), + JSON.stringify({ ok: true, value }), + ); + return { command: process.execPath, arguments: ["-e", ""] }; + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }), + ); + expect(result.ok).toBe(matching); + if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError"); + else expect(result.value.decoder).toEqual(HAR_CAPTURE_PROVIDER_IDENTITY); + }, +); + +it("classifies an oversized selected capture before creating an owned decoder", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-limit-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "input.har"); + await writeFile(path, ""); + await truncate(path, WEB_NETWORK_CAPTURE_LIMITS.inputBytes + 1); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async () => { + throw new Error("Oversized input must not launch decoding"); + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }), + ); + if (result.ok) throw new Error("Oversized input must fail"); + expect(result.error._tag).toBe("AnalysisInputError"); + expect(projectAnalysisError(result.error)).toMatchObject({ + category: "invalid_input", + details: { + issues: [ + { + path: ["capture_path"], + reason: "out_of_range", + expected: { + maximum_capture_bytes: WEB_NETWORK_CAPTURE_LIMITS.inputBytes, + }, + }, + ], + }, + }); +}); + +it("does not misclassify decoder filesystem failures as missing selected input", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-command-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "input.har"); + await writeFile(path, JSON.stringify(historicalHar())); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async () => { + throw Object.assign( + new Error("Configured decoder command file missing"), + { code: "ENOENT" }, + ); + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }), + ); + if (result.ok) throw new Error("Failed decoder command must fail"); + expect(result.error._tag).toBe("ProviderAdapterError"); + expect(projectAnalysisError(result.error)).toMatchObject({ + details: { + diagnostics: { + phase: "decoder", + reason: "Configured decoder command file missing", + }, + }, + }); +}); + +it.each(["missing-capture", "missing-reply", "failed-command"])( + "redacts declared paths and diagnostics while preserving the %s failure", + async (mode) => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-sensitive-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "explicit-private-value.har"); + if (mode !== "missing-capture") + await writeFile(path, JSON.stringify(historicalHar())); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async () => ({ + command: process.execPath, + arguments: [ + "-e", + mode === "failed-command" + ? 'process.stderr.write("explicit-private-value ordinary diagnostic"); process.exit(2)' + : "process.exit(0)", + ], + }), + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + sensitive_values: ["explicit-private-value", "REDACTED"], + }), + ); + if (result.ok) throw new Error("Failure required"); + const projected = projectAnalysisError(result.error); + const serialized = JSON.stringify(projected); + expect(serialized).not.toContain("explicit-private-value"); + expect(serialized).not.toContain("REDACTED"); + expect(result.error._tag).toBe( + mode === "missing-capture" + ? "AnalysisInputError" + : mode === "missing-reply" + ? "AnalysisOutputError" + : "ProviderAdapterError", + ); + if (mode === "missing-capture") expect(serialized).toContain("ENOENT"); + if (mode === "failed-command") + expect(serialized).toContain('"exit_code":2'); + }, +); + +it("reports an absent owned reply as output failure and removes its private snapshot root", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-decoder-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "input.har"); + await writeFile(path, JSON.stringify(historicalHar())); + let privatePath: string | undefined; + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async (_, runtimePath) => { + privatePath = runtimePath; + return { + command: process.execPath, + arguments: ["-e", "process.exit(0)"], + }; + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }), + ); + if (result.ok) throw new Error("Owned reply is required"); + expect(result.error._tag).toBe("AnalysisOutputError"); + expect(result.error.message).toContain("ENOENT"); + if (privatePath === undefined) + throw new Error("Private root was not acquired"); + await expect(access(privatePath)).rejects.toMatchObject({ code: "ENOENT" }); +}); + +it("cancels after snapshot acquisition before launch and removes private input declarations", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-cancel-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "input.har"); + await writeFile(path, JSON.stringify(historicalHar())); + let privatePath: string | undefined; + const controller = new AbortController(); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async (_, runtimePath) => { + privatePath = runtimePath; + controller.abort(); + return { + command: process.execPath, + arguments: ["-e", "process.exit(3)"], + }; + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + sensitive_values: ["explicit-private-declaration"], + }), + { signal: controller.signal }, + ); + if (result.ok) throw new Error("Cancelled acquisition must fail"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + if (privatePath === undefined) + throw new Error("Private root was not acquired"); + await expect(access(privatePath)).rejects.toMatchObject({ code: "ENOENT" }); +}); + +it.each([ + { reason: "input-limit", tag: "AnalysisInputError" }, + { reason: "resource-limit", tag: "ProviderAdapterError" }, + { reason: "decoder", tag: "ProviderAdapterError" }, + { reason: "limit", tag: "AnalysisOutputError" }, +])( + "preserves $reason decoder failures through the public error boundary", + async ({ reason, tag }) => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-reply-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "input.har"); + await writeFile(path, JSON.stringify(historicalHar())); + let privatePath: string | undefined; + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async (_, runtimePath) => { + privatePath = runtimePath; + await writeFile( + join(runtimePath, "reply.json"), + JSON.stringify({ + ok: false, + reason, + message: "Specific decoder constraint", + pointer: "/extension/child", + }), + ); + return { + command: process.execPath, + arguments: ["-e", "process.exit(0)"], + }; + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }), + ); + if (result.ok) throw new Error("Failure required"); + expect(result.error._tag).toBe(tag); + const projected = projectAnalysisError(result.error); + if (reason === "input-limit") { + expect(projected).toMatchObject({ + category: "invalid_input", + details: { + issues: [ + { + path: ["capture_path", "/extension/child"], + reason: "out_of_range", + expected: { + maximum_capture_nesting: WEB_NETWORK_CAPTURE_LIMITS.depth, + }, + }, + ], + }, + }); + } else if (tag === "ProviderAdapterError") { + expect(projected).toMatchObject({ + details: { + diagnostics: { + phase: "decoder", + reason: "Specific decoder constraint", + failure_kind: reason, + pointer: "/extension/child", + }, + }, + }); + } + if (privatePath === undefined) + throw new Error("Private root was not acquired"); + await expect(access(privatePath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( + "reports a real filesystem permission denial separately from missing capture input", + async () => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-denied-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "unreadable.har"); + await writeFile(path, JSON.stringify(historicalHar()), { mode: 0o000 }); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async () => { + throw new Error("Permission-denied input must not launch decoding"); + }, + }, + ], + process.env, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + }), + ); + await chmod(path, 0o600); + if (result.ok) throw new Error("Permission denial required"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "access_denied", + category: "unavailable", + retryable: false, + details: { + operation: "inspect_web_network_capture", + path, + system_code: "EACCES", + }, + }); + expect(projectAnalysisError(result.error).remediation.action).toContain( + "read access", + ); + }, +); + +it.each(["format", "process"])( + "preserves the structured %s failure when runtime cleanup fails", + async (kind) => { + const root = await mkdtemp(join(tmpdir(), "rea-historical-cleanup-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "capture-private.har"); + await writeFile(path, JSON.stringify(historicalHar())); + const decoder = new HistoricalCaptureDecoder( + [ + { + format: "har", + identity: HAR_CAPTURE_PROVIDER_IDENTITY, + command: async (_, runtimePath) => { + if (kind === "format") + await writeFile( + join(runtimePath, "reply.json"), + JSON.stringify({ + ok: false, + reason: "format", + message: "Malformed retained record", + pointer: "/metadata/capture-private", + }), + ); + return { + command: process.execPath, + arguments: [ + "-e", + kind === "format" + ? "process.exit(0)" + : 'process.stderr.write("Specific provider fault"); process.exit(3)', + ], + }; + }, + }, + ], + process.env, + async () => { + const runtime = await PrivateRuntimeRoot.create({ + prefix: "rea-cleanup-proof-", + }); + onTestFinished(() => runtime.close()); + return { + path: runtime.path, + close: async () => { + throw new Error("Deliberate cleanup denial"); + }, + }; + }, + ); + const result = await decoder.inspect( + inspectWebNetworkCaptureInputSchema.parse({ + capture_path: path, + format: "har", + sensitive_values: ["capture-private"], + }), + ); + if (result.ok) throw new Error("Cleanup failure required"); + const projected = projectAnalysisError(result.error); + expect(projected).toMatchObject({ + code: "cleanup_incomplete", + details: { + diagnostics: { + previous_error: + kind === "format" + ? { + category: "invalid_input", + details: { + issues: [ + { + path: ["capture_path", "/metadata"], + reason: "invalid_format", + message: "Malformed retained record", + }, + ], + }, + } + : { + code: "execution_failure", + details: { + diagnostics: { + phase: "decoder", + exit_code: 3, + stderr: "Specific provider fault", + }, + }, + }, + }, + }, + }); + expect(JSON.stringify(projected)).not.toContain("capture-private"); + }, +); diff --git a/tests/boundary/browser/historicalWebCapture.test.ts b/tests/boundary/browser/historicalWebCapture.test.ts new file mode 100644 index 0000000000..a6905bdb3b --- /dev/null +++ b/tests/boundary/browser/historicalWebCapture.test.ts @@ -0,0 +1,109 @@ +import { createHash } from "node:crypto"; +import { mkdtemp, writeFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { expect, it, onTestFinished } from "vitest"; +import { execFile } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +const entrypoint = fileURLToPath( + new URL("../../../scripts/rea.mjs", import.meta.url), +); +const cli = async ( + path: string, + format: string, + extra: readonly string[] = [], +) => { + try { + const result = await promisify(execFile)( + process.execPath, + [ + entrypoint, + "inspect-web-network-capture", + path, + format, + "--json", + ...extra, + ], + { env: { ...process.env, REA_MITMDUMP_COMMAND: "" }, timeout: 40_000 }, + ); + return { ok: true, stdout: result.stdout }; + } catch (cause: unknown) { + if ( + cause instanceof Error && + "stdout" in cause && + typeof cause.stdout === "string" + ) + return { ok: false, stdout: cause.stdout }; + throw cause; + } +}; +import { historicalHar } from "../../fixtures/historicalHar.js"; + +it("reads actual upstream HAR packages in an owned process and retains selected producer order", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-history-har-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const fixture = historicalHar(); + const first = fixture.log.entries[0]; + if (first === undefined) throw new Error("fixture missing"); + fixture.log.entries.push(structuredClone(first)); + const text = JSON.stringify(fixture); + const path = join(root, "capture.har"); + await writeFile(path, text); + const called = await cli(path, "har", ["--record", "1", "--record", "0"]); + expect(called.ok, called.stdout).toBe(true); + const result = { value: parseEvidence(JSON.parse(called.stdout)) }; + expect(result.value.subject?.digest).toMatchObject({ + sha256: createHash("sha256").update(text).digest("hex"), + }); + expect(result.value.normalized_result).toMatchObject({ + total_records: 2, + records: [{ ordinal: 1 }, { ordinal: 0 }], + runtime_attribution: "unknown", + }); +}); + +it("keeps malformed JSON separate from absent external mitmproxy capability", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-history-errors-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const path = join(root, "capture.har"); + await writeFile(path, "{malformed-private-json"); + const malformed = await cli(path, "har"); + expect(malformed.ok).toBe(false); + expect(JSON.parse(malformed.stdout)).toMatchObject({ + category: "invalid_input", + }); + expect(malformed.stdout).not.toContain("malformed-private-json"); + const missing = await cli(path, "mitmproxy"); + expect(missing.ok).toBe(false); + expect(JSON.parse(missing.stdout)).toMatchObject({ + category: "unsupported_provider", + }); +}); + +it("reports the actual HAR decoder nesting constraint as a typed selected-input issue", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-history-depth-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + let extension: unknown = "leaf"; + for (let depth = 0; depth < 66; depth++) extension = { child: extension }; + const path = join(root, "deep.har"); + await writeFile( + path, + JSON.stringify({ ...historicalHar(), _extension: extension }), + ); + const result = await cli(path, "har"); + expect(result.ok).toBe(false); + expect(JSON.parse(result.stdout)).toMatchObject({ + category: "invalid_input", + details: { + issues: [ + { + path: ["capture_path", expect.stringMatching(/^\/_extension\/child/)], + reason: "out_of_range", + expected: { maximum_capture_nesting: 64 }, + }, + ], + }, + }); +}); diff --git a/tests/boundary/browser/webCaptureDiff.test.ts b/tests/boundary/browser/webCaptureDiff.test.ts index 813b4d680f..935bc7ae5a 100644 --- a/tests/boundary/browser/webCaptureDiff.test.ts +++ b/tests/boundary/browser/webCaptureDiff.test.ts @@ -22,16 +22,6 @@ afterEach(async () => { }); describe("web capture diff", () => { - it("rejects caller limits that would omit difference records", () => { - expect( - compareWebCapturesInputSchema.safeParse({ - before: {}, - after: {}, - max_changes: 1, - }).success, - ).toBe(false); - }); - it("reports stable observed changes while preserving unknown dimensions", async () => { const browser = await startFakeCdpBrowser(); browsers.push(browser); @@ -48,7 +38,22 @@ describe("web capture diff", () => { const after = structuredClone(captured.value); after.scripts.items = []; const request = after.network.requests[0]; - if (request !== undefined) request.status = 204; + if (request !== undefined) { + request.status = 204; + request.redirects = [ + { + url: `${browser.allowedOrigin}/prior`, + response_url: `${browser.allowedOrigin}/prior`, + method: "GET", + resource_type: "Fetch", + status: 302, + mime_type: "text/plain", + encoded_data_length: 12, + request_timestamp: 1, + redirect_event_timestamp: 2, + }, + ]; + } const result = compareWebCaptures( compareWebCapturesInputSchema.parse({ @@ -163,6 +168,68 @@ describe("web capture diff", () => { }); }); +describe("web capture redirect comparison", () => { + it("ignores redirect event times but compares redirect semantics", async () => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const captured = await new CdpBrowserProvider().inspectPage( + inspectWebPageInputSchema.parse({ + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + target_id: "allowed-page", + observation_ms: 0, + }), + ); + if (!captured.ok) throw captured.error; + const before = structuredClone(captured.value); + const beforeRequest = before.network.requests[0]; + if (beforeRequest === undefined) throw new Error("Missing network request"); + beforeRequest.redirects = [ + { + url: `${browser.allowedOrigin}/prior`, + response_url: `${browser.allowedOrigin}/prior`, + method: "GET", + resource_type: "Fetch", + status: 302, + mime_type: "text/plain", + encoded_data_length: 12, + request_timestamp: 1, + redirect_event_timestamp: 2, + }, + ]; + markSectionsComplete(before, ["network_requests"]); + + const after = structuredClone(before); + const afterRequest = after.network.requests[0]; + if (afterRequest === undefined) throw new Error("Missing network request"); + const hop = afterRequest.redirects?.[0]; + if (hop === undefined) throw new Error("Missing redirect hop"); + hop.request_timestamp = 10; + hop.redirect_event_timestamp = 20; + + const compare = (inspection: typeof before) => + compareWebCaptures( + compareWebCapturesInputSchema.parse({ + before: { inspection: before }, + after: { inspection }, + }), + ).dimensions.network; + expect(compare(after)).toMatchObject({ + status: "unchanged", + total_changes: 0, + }); + + const changed = structuredClone(after); + const changedHop = changed.network.requests[0]?.redirects?.[0]; + if (changedHop === undefined) throw new Error("Missing redirect hop"); + changedHop.status = 307; + expect(compare(changed)).toMatchObject({ + status: "changed", + changes: [expect.objectContaining({ change: "modified" })], + }); + }); +}); + describe("web capture diff incomplete inventories", () => { it.each(["truncated_sections", "unavailable_sections"] as const)( "does not infer script additions or removals from %s", diff --git a/tests/boundary/cli/browserCli.test.ts b/tests/boundary/cli/browserCli.test.ts index 6aab7fe3e8..9ddd0ac9cb 100644 --- a/tests/boundary/cli/browserCli.test.ts +++ b/tests/boundary/cli/browserCli.test.ts @@ -108,6 +108,57 @@ describe("browser CLI parity", () => { alignment: { status: "aligned", aligned_steps: 2 }, }, }); + const intentionalPolicy = await runCli( + [ + "compare-web-captures", + JSON.stringify(scenarioCapture), + JSON.stringify(scenarioCapture), + "--normalization-json", + JSON.stringify({ + rules: [ + { + rule_id: "domain", + artifacts: ["url"], + match: "app.example.test", + replacement: "normalized.test", + }, + ], + }), + "--json", + ], + process.env, + ); + expect(intentionalPolicy).toMatchObject({ + normalized_result: { + normalization: { rules: [{ rule_id: "domain" }] }, + }, + }); + + const malformedPolicy = await runCli( + [ + "compare-web-captures", + JSON.stringify(scenarioCapture), + JSON.stringify(scenarioCapture), + "--normalization-json", + "{invalid-policy", + "--json", + ], + process.env, + ); + expect(malformedPolicy).toMatchObject({ + error: "Browser observation failed", + code: "invalid_request", + details: { + operation: "compare_web_captures", + issues: [ + { + path: ["normalization"], + reason: "invalid_format", + expected: "JSON", + }, + ], + }, + }); }, INTEGRATION_TEST_TIMEOUT_MS, ); diff --git a/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts b/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts new file mode 100644 index 0000000000..6608ce08d7 --- /dev/null +++ b/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts @@ -0,0 +1,113 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +import { afterEach, expect, it } from "vitest"; + +import { + startFakeCdpBrowser, + type FakeCdpBrowser, +} from "../../fixtures/fakeCdpBrowser.js"; + +const execute = promisify(execFile); +const browsers: FakeCdpBrowser[] = []; + +afterEach(async () => { + await Promise.all(browsers.splice(0).map(async (browser) => browser.close())); +}); + +it("reports missing JSON shapes only when body shapes are selected", async () => { + const browser = await startFakeCdpBrowser({ + malformedRedirectResponse: true, + redirectResponseEnvelope: null, + }); + browsers.push(browser); + const commonArgs = [ + "inspect-web-page", + browser.endpoint, + "allowed-page", + "--allowed-origins", + browser.allowedOrigin, + "--observation-ms", + "0", + ]; + const selected = normalizedResult( + await runCli([...commonArgs, "--include-json-body-shapes", "--json"]), + ); + expect(selected).toMatchObject({ + network: { + requests: [ + { + body_shapes: { + status: "partial", + request: expect.any(Object), + response: null, + }, + }, + ], + }, + completeness: { + unavailable_sections: expect.arrayContaining([ + "network_requests", + "json_body_shapes", + ]), + excluded: expect.arrayContaining([ + { + section: "json_body_shapes", + reason: "invalid_protocol_value", + count: expect.any(Number), + }, + ]), + }, + }); + + const defaulted = normalizedResult(await runCli([...commonArgs, "--json"])); + expect(defaulted).toMatchObject({ + network: { + requests: [ + { + body_shapes: { + status: "not_approved", + request: null, + response: null, + }, + }, + ], + }, + }); + expect(defaulted.completeness).toMatchObject({ + unavailable_sections: expect.not.arrayContaining(["json_body_shapes"]), + }); +}); + +const runCli = async (arguments_: readonly string[]): Promise => { + try { + const { stdout } = await execute( + process.execPath, + ["scripts/rea.mjs", ...arguments_], + { + cwd: process.cwd(), + env: process.env, + maxBuffer: 16 * 1_024 * 1_024, + }, + ); + return JSON.parse(stdout); + } catch (cause: unknown) { + if ( + typeof cause === "object" && + cause !== null && + "stdout" in cause && + typeof cause.stdout === "string" + ) + return JSON.parse(cause.stdout); + throw cause; + } +}; + +const normalizedResult = (value: unknown): Record => { + if (!isRecord(value) || !isRecord(value.normalized_result)) + throw new TypeError("Missing CLI normalized result"); + return value.normalized_result; +}; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts index 22fa1b0a65..783097b022 100644 --- a/tests/boundary/cli/cliOutput.test.ts +++ b/tests/boundary/cli/cliOutput.test.ts @@ -57,7 +57,9 @@ describe("CLI output argument and sanitization boundary", () => { validateCliOutputArguments(["providers", "--token-count", "--json"]), ).toEqual({ ok: true }); }); +}); +describe("CLI output valued-flag parsing", () => { workspaceCliTest( "preserves complete JSON errors when native builtin flags follow unusual arguments", async ({ cli }) => { @@ -92,6 +94,70 @@ describe("CLI output argument and sanitization boundary", () => { CLI_INTEGRATION_TIMEOUT_MS, ); + workspaceCliTest( + "does not treat empty or equals-style output values as active builtins", + async ({ cli }) => { + for (const arguments_ of [ + ["capabilities", "--json", "--format", ""], + ["capabilities", "--json", "--filter-output", ""], + ["capabilities", "--json", "--token-limit", ""], + ["capabilities", "--json", "--token-limit=5"], + ]) { + const result = await cli.run({ arguments: arguments_ }); + expect(result.exitCode).toBe(1); + expect(result.json).toBeDefined(); + expect(result.stdout).not.toContain("[truncated:"); + expect(JSON.stringify(result.json)).not.toContain( + "UNSUPPORTED_OUTPUT_COMBINATION", + ); + } + const validTokenWindow = await cli.run({ + arguments: ["capabilities", "--json", "--token-limit", "5"], + }); + expect(validTokenWindow.exitCode).toBe(1); + expect(validTokenWindow.json).toMatchObject({ + ok: false, + error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" }, + }); + const emptyFormatWithTokenWindow = await cli.run({ + arguments: [ + "capabilities", + "--json", + "--format", + "", + "--token-limit", + "5", + ], + }); + expect(emptyFormatWithTokenWindow.exitCode).toBe(1); + expect(emptyFormatWithTokenWindow.stdout).not.toContain("[truncated:"); + expect(emptyFormatWithTokenWindow.json).toMatchObject({ + ok: false, + error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" }, + }); + + for (const arguments_ of [ + ["capabilities", "--json", "--token-limit", "not-a-number"], + [ + "capabilities", + "--format", + "unsupported-format", + "--json", + "--token-limit", + "5", + ], + ]) { + const result = await cli.run({ arguments: arguments_ }); + expect(result.exitCode).toBe(1); + expect(result.stdout).not.toContain("UNSUPPORTED_OUTPUT_COMBINATION"); + expect(result.stdout).not.toContain("[truncated:"); + } + }, + CLI_INTEGRATION_TIMEOUT_MS, + ); +}); + +describe("CLI output argument and sanitization boundary", () => { it("preserves normal output and sanitizes text and JSON validation errors", () => { expect(sanitizeCliOutput("result: ok\n")).toBe("result: ok\n"); expect(sanitizeCliOutput("result: VALIDATION_ERROR\n")).toBe( @@ -179,7 +245,7 @@ describe("compiled CLI output boundary", () => { expect(result).toMatchObject({ exitCode: 0, stdout: "{}\n", - stderr: "", + stderr: expect.stringContaining('"rea_progress":'), json: {}, }); const nested = await cli.run({ @@ -194,7 +260,7 @@ describe("compiled CLI output boundary", () => { }); expect(nested).toMatchObject({ exitCode: 0, - stderr: "", + stderr: expect.stringContaining('"rea_progress":'), json: { normalized_result: {} }, }); const selected = await cli.run({ @@ -209,7 +275,7 @@ describe("compiled CLI output boundary", () => { }); expect(selected).toMatchObject({ exitCode: 0, - stderr: "", + stderr: expect.stringContaining('"rea_progress":'), json: { normalized_result: { summary: expect.objectContaining({ browser_windows: 0 }), diff --git a/tests/boundary/cli/dispatcher.test.ts b/tests/boundary/cli/dispatcher.test.ts index d94cb7ec75..79d2913b99 100644 --- a/tests/boundary/cli/dispatcher.test.ts +++ b/tests/boundary/cli/dispatcher.test.ts @@ -11,6 +11,7 @@ describe("executable dispatcher", () => { async ({ processes, workspace }) => { const scripts = await workspace.mkdir("scripts"); await workspace.mkdir("dist"); + await workspace.mkdir("dist/cli"); await Promise.all([ copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")), workspace.write( @@ -29,6 +30,10 @@ describe("executable dispatcher", () => { "dist/cliOutput.js", "export const sanitizeCliOutput = (output) => output; export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined;\n", ), + workspace.write( + "dist/cli/streamedJsonOutput.js", + "export const createStreamedCliJsonOutput = () => undefined;\n", + ), workspace.write( "dist/mcpDoctor.js", 'export const runProductionMcpDoctorCli = (args) => Promise.resolve({ output: JSON.stringify({ adapter: "mcp-doctor", args }), exitCode: 0 });\n', diff --git a/tests/boundary/cli/dylibResolutionCli.test.ts b/tests/boundary/cli/dylibResolutionCli.test.ts new file mode 100644 index 0000000000..1911d85517 --- /dev/null +++ b/tests/boundary/cli/dylibResolutionCli.test.ts @@ -0,0 +1,244 @@ +import { chmod, mkdir, symlink, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import { describe, expect } from "vitest"; + +import { + FILE_TYPE, + LC, + dylibCommand, + machoImage, + rpathCommand, +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; +import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const ENVIRONMENT = { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" }; + +const writeFiles = async ( + root: string, + files: Readonly>, +): Promise => { + for (const [path, content] of Object.entries(files)) { + await mkdir(dirname(join(root, path)), { recursive: true }); + await writeFile(join(root, path), content); + } +}; + +const fixtureApp = async (): Promise => { + const app = join( + await createTestTempDirectory("rea-dylib-cli-"), + "Fixture.app", + ); + await writeFiles(app, { + "Contents/Info.plist": + "CFBundleExecutableApp", + "Contents/MacOS/App": machoImage({ + commands: [ + rpathCommand("@executable_path/../Frameworks"), + dylibCommand(LC.LOAD_DYLIB, "@rpath/libcore.dylib"), + dylibCommand(LC.LOAD_WEAK_DYLIB, "@rpath/libgone.dylib"), + ], + }), + "Contents/Frameworks/libcore.dylib": machoImage({ + fileType: FILE_TYPE.dylib, + commands: [dylibCommand(LC.ID_DYLIB, "@rpath/libcore.dylib")], + }), + "Contents/Helpers/tool": machoImage({ + commands: [dylibCommand(LC.LOAD_DYLIB, "@loader_path/libnone.dylib")], + }), + }); + return app; +}; + +const withBrokenHelper = async (app: string): Promise => { + const broken = machoImage({ commands: [rpathCommand("x")] }); + // cmdsize 0 makes the command table unreadable. + new DataView(broken.buffer).setUint32(32 + 4, 0, true); + await writeFiles(app, { "Contents/Helpers/broken": broken }); +}; + +describe("trace-dylib-resolution CLI", () => { + cliTest("traces every executable root of an app bundle", async ({ cli }) => { + const app = await fixtureApp(); + const result = await cli.run({ + arguments: ["trace-dylib-resolution", app, "--json"], + environment: ENVIRONMENT, + }); + expect(result.exitCode, JSON.stringify(result.json)).toBe(0); + const trace = dylibResolutionResultSchema.parse( + (result.json as { normalized_result: unknown }).normalized_result, + ); + expect(trace.root_path).toBe(app); + expect(trace.roots.map(({ image }) => image)).toEqual([ + "Contents/Helpers/tool", + "Contents/MacOS/App", + ]); + expect( + trace.edges.map(({ loader, install_name: name, resolution }) => [ + loader, + name, + resolution.status, + resolution.image, + ]), + ).toEqual([ + [ + "Contents/Helpers/tool", + "@loader_path/libnone.dylib", + "unresolved", + null, + ], + [ + "Contents/MacOS/App", + "@rpath/libcore.dylib", + "resolved", + "Contents/Frameworks/libcore.dylib", + ], + ["Contents/MacOS/App", "@rpath/libgone.dylib", "unresolved", null], + ]); + expect(trace.findings.map(({ kind }) => kind)).toEqual([ + "required-load-unresolved", + "weak-load-unresolved", + ]); + expect( + trace.images.every(({ sha256 }) => /^[0-9a-f]{64}$/u.test(sha256)), + ).toBe(true); + }); + + cliTest( + "narrows roots and architecture, and rejects escaping roots", + async ({ cli }) => { + const app = await fixtureApp(); + const narrowed = await cli.run({ + arguments: [ + "trace-dylib-resolution", + app, + "--root", + "Contents/MacOS/App", + "--architecture", + "arm64", + "--json", + ], + environment: ENVIRONMENT, + }); + expect(narrowed.exitCode).toBe(0); + expect( + (narrowed.json as { normalized_result: { roots: unknown } }) + .normalized_result.roots, + ).toEqual([{ image: "Contents/MacOS/App", architecture: "arm64" }]); + // A root below a symlinked directory that points outside the bundle. + const outside = await createTestTempDirectory("rea-dylib-outside-"); + await writeFiles(outside, { Tool: machoImage({}) }); + await symlink(outside, join(app, "Contents/Linked")); + for (const root of [ + "../outside", + "Contents/Info.plist", + "Contents/Linked/Tool", + ]) { + const rejected = await cli.run({ + arguments: ["trace-dylib-resolution", app, "--root", root, "--json"], + environment: ENVIRONMENT, + }); + expect(rejected.exitCode).toBe(1); + expect(rejected.json).toMatchObject({ error: "Analysis failed" }); + } + }, + ); + + cliTest( + "resolves a standalone Mach-O against its own directory", + async ({ cli }) => { + const directory = await createTestTempDirectory("rea-dylib-standalone-"); + await writeFiles(directory, { + tool: machoImage({ + commands: [ + dylibCommand(LC.LOAD_DYLIB, "@loader_path/lib/libhelper.dylib"), + dylibCommand(LC.LOAD_DYLIB, "@loader_path/../escape.dylib"), + ], + }), + "lib/libhelper.dylib": machoImage({ fileType: FILE_TYPE.dylib }), + "unrelated.txt": "not traversed", + }); + const result = await cli.run({ + arguments: [ + "trace-dylib-resolution", + join(directory, "tool"), + "--json", + ], + environment: ENVIRONMENT, + }); + expect(result.exitCode, JSON.stringify(result.json)).toBe(0); + const trace = dylibResolutionResultSchema.parse( + (result.json as { normalized_result: unknown }).normalized_result, + ); + expect(trace.roots).toEqual([{ image: "tool", architecture: "arm64" }]); + expect(trace.edges.map(({ resolution }) => resolution)).toEqual([ + { status: "resolved", image: "lib/libhelper.dylib" }, + { status: "undetermined", image: null }, + ]); + }, + ); + + cliTest( + "reports unparsed Mach-O files found while enumerating roots", + async ({ cli }) => { + const app = await fixtureApp(); + await withBrokenHelper(app); + const result = await cli.run({ + arguments: ["trace-dylib-resolution", app, "--json"], + environment: ENVIRONMENT, + }); + expect(result.exitCode, JSON.stringify(result.json)).toBe(0); + const trace = dylibResolutionResultSchema.parse( + (result.json as { normalized_result: unknown }).normalized_result, + ); + expect(trace.coverage).toMatchObject({ + status: "partial", + unparsed_images: ["Contents/Helpers/broken"], + }); + }, + ); +}); + +describe.skipIf(process.getuid?.() === 0)( + "trace-dylib-resolution permissions", + () => { + cliTest( + "reports an unreadable dependency as a permission failure", + async ({ cli }) => { + const directory = await createTestTempDirectory("rea-dylib-denied-"); + await writeFiles(directory, { + tool: machoImage({ + commands: [ + dylibCommand(LC.LOAD_DYLIB, "@loader_path/libsecret.dylib"), + ], + }), + "libsecret.dylib": machoImage({ fileType: FILE_TYPE.dylib }), + }); + await chmod(join(directory, "libsecret.dylib"), 0o000); + try { + const result = await cli.run({ + arguments: [ + "trace-dylib-resolution", + join(directory, "tool"), + "--json", + ], + environment: ENVIRONMENT, + }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + details: { + reason: "unavailable", + detail: expect.stringContaining( + "Permission denied (EACCES) reading libsecret.dylib", + ), + }, + }); + } finally { + await chmod(join(directory, "libsecret.dylib"), 0o644); + } + }, + ); + }, +); diff --git a/tests/boundary/cli/javascriptApplicationJsonOutput.test.ts b/tests/boundary/cli/javascriptApplicationJsonOutput.test.ts new file mode 100644 index 0000000000..09184e1377 --- /dev/null +++ b/tests/boundary/cli/javascriptApplicationJsonOutput.test.ts @@ -0,0 +1,158 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { createPackageWithOptions } from "@electron/asar"; +import { Filter } from "incur"; +import { expect } from "vitest"; +import { z } from "zod"; + +import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const filter = "evidence_id,normalized_result.graph.nodes[0:1].node_id"; + +for (const command of ["analyze-javascript-application", "analyze"]) { + for (const format of ["json", "jsonl"]) { + for (const fullOutput of [false, true]) { + for (const filtered of [false, true]) { + cliTest( + `${command} preserves ${format}, full output ${String(fullOutput)}, filtering ${String(filtered)}`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-json-output-"); + await writeFile(join(root, "main.js"), "fetch('');\n"); + const direct = await analyzeJavaScriptApplication({ + input_path: root, + }); + if (!direct.ok) throw direct.error; + const output = await cli.run({ + arguments: [ + command, + root, + "--format", + format, + ...(fullOutput ? ["--full-output"] : []), + ...(filtered ? ["--filter-output", filter] : []), + ], + }); + expect(output.exitCode).toBe(0); + let data: unknown = output.json; + if (fullOutput) { + const envelope = z + .object({ + ok: z.literal(true), + data: z.unknown(), + meta: z.object({ + command: z.literal(command), + duration: z.string().regex(/^\d+ms$/u), + }), + }) + .parse(output.json); + data = envelope.data; + } + expect(data).toEqual( + filtered + ? Filter.apply(direct.value, Filter.parse(filter)) + : direct.value, + ); + }, + ); + } + } + } +} + +cliTest( + "preserves a complete empty filtered JSON document", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-json-empty-filter-"); + await writeFile(join(root, "main.js"), "export const value = 1;\n"); + const output = await cli.run({ + arguments: [ + "analyze-javascript-application", + root, + "--json", + "--filter-output", + "absent.field", + ], + }); + expect(output.exitCode).toBe(0); + expect(output.json).toEqual({}); + }, +); + +cliTest( + "preserves a typed failed analysis in the JSON result", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-json-failure-"); + const output = await cli.run({ + arguments: [ + "analyze-javascript-application", + join(root, "absent"), + "--json", + ], + }); + expect(output.json).toMatchObject({ + code: "artifact_operation_failed", + details: { reason: "io" }, + }); + }, +); + +cliTest( + "preserves JSON omission for inherited filter methods", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-json-inherited-filter-"); + await writeFile(join(root, "main.js"), "export const value = 1;\n"); + const output = await cli.run({ + arguments: [ + "analyze-javascript-application", + root, + "--json", + "--filter-output", + "toString", + ], + }); + expect(output.exitCode).toBe(0); + expect(output.json).toEqual({}); + }, +); + +for (const command of ["analyze", "analyze-javascript-application"]) { + cliTest( + `${command} forwards the integrity policy while preserving JSON output`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-cli-integrity-"); + const source = join(root, "source"); + await mkdir(source); + await writeFile(join(source, "main.js"), "require('./addon.node');\n"); + await writeFile(join(source, "addon.node"), "original"); + const archive = join(root, "app.asar"); + await createPackageWithOptions(source, archive, { unpack: "*.node" }); + await writeFile(join(`${archive}.unpacked`, "addon.node"), "changed"); + const strict = await cli.run({ arguments: [command, archive, "--json"] }); + expect(strict.json).toMatchObject({ + code: "artifact_integrity_mismatch", + }); + const recorded = await cli.run({ + arguments: [ + command, + archive, + "--integrity-policy", + "record-and-continue", + "--json", + ], + }); + expect(recorded.exitCode).toBe(0); + expect(recorded.json).toMatchObject({ + parameters: { integrity_policy: "record-and-continue" }, + normalized_result: { + integrity_contradictions: [ + { logical_path: "addon.node", trust: "observed-untrusted" }, + ], + graph: { coverage: { status: "partial" } }, + }, + }); + }, + ); +} diff --git a/tests/boundary/cli/javascriptApplicationProgress.test.ts b/tests/boundary/cli/javascriptApplicationProgress.test.ts new file mode 100644 index 0000000000..a4a0863e4d --- /dev/null +++ b/tests/boundary/cli/javascriptApplicationProgress.test.ts @@ -0,0 +1,72 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect } from "vitest"; +import { z } from "zod"; + +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const progressLine = z.object({ + rea_progress: z.object({ + phase: z.string(), + completed: z.number(), + total: z.number().nullable(), + message: z.string(), + sequence: z.number(), + terminal: z.boolean().optional(), + }), +}); + +for (const command of ["analyze-javascript-application", "analyze"]) { + cliTest( + `${command} reports real analysis phases on stderr and preserves stdout Evidence`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-cli-progress-"); + await writeFile(join(root, "main.js"), "fetch('');\n"); + const direct = await analyzeJavaScriptApplication({ input_path: root }); + if (!direct.ok) throw direct.error; + + const output = await cli.run({ arguments: [command, root, "--json"] }); + expect(output.exitCode).toBe(0); + expect(parseEvidence(output.json).evidence_id).toBe( + direct.value.evidence_id, + ); + const updates = output.stderr + .trim() + .split("\n") + .map((line) => progressLine.parse(JSON.parse(line)).rea_progress); + expect(updates[0]?.completed).toBe(0); + expect( + updates.some(({ phase }) => phase === "parse_javascript_sources"), + ).toBe(true); + expect(updates.at(-1)).toMatchObject({ completed: 1, terminal: true }); + expect( + updates.slice(0, -1).every(({ terminal }) => terminal !== true), + ).toBe(true); + }, + ); +} + +cliTest( + "keeps a failed analysis distinct from a completed result", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-js-cli-progress-failure-"); + const output = await cli.run({ + arguments: [ + "analyze-javascript-application", + join(root, "absent"), + "--json", + ], + }); + expect(output.json).toMatchObject({ code: "artifact_operation_failed" }); + const updates = output.stderr + .trim() + .split("\n") + .map((line) => progressLine.parse(JSON.parse(line)).rea_progress); + expect(updates[0]?.completed).toBe(0); + expect(updates.every(({ terminal }) => terminal !== true)).toBe(true); + }, +); diff --git a/tests/boundary/cli/jsonInputStatus.test.ts b/tests/boundary/cli/jsonInputStatus.test.ts index f95c9d54b5..1acd8c61b0 100644 --- a/tests/boundary/cli/jsonInputStatus.test.ts +++ b/tests/boundary/cli/jsonInputStatus.test.ts @@ -1,9 +1,8 @@ -import { writeFile } from "node:fs/promises"; +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect } from "vitest"; -import { parseCliJsonInput } from "../../../src/cliJsonInput.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; @@ -30,46 +29,63 @@ const JSON_COMMANDS = [ describe("compiled CLI JSON input failure status", () => { for (const [command, operation] of JSON_COMMANDS) - for (const kind of ["inline", "file", "missing"] as const) - cliTest( - `${command} rejects malformed or unreadable ${kind} input before dispatch`, - async ({ cli }) => { - const root = await createTestTempDirectory("rea-cli-json-status-"); - const input = kind === "inline" ? "{" : join(root, "input.json"); - if (kind === "file") await writeFile(input, "{"); - const parsed = await parseCliJsonInput(input, operation); - if (parsed.ok) - throw new Error("Expected JSON input to fail before dispatch"); + cliTest( + `${command} rejects malformed inline input before dispatch`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cli-json-status-"); + const input = "{"; + const result = await cli.run({ + arguments: [command, input, "--json"], + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); - const result = await cli.run({ - arguments: [command, input, "--json"], - environment: { - HOME: root, - XDG_CONFIG_HOME: root, - XDG_CACHE_HOME: root, - }, - }); + expect(result.json).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { operation }, + }); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(1); + }, + ); - expect(result.stdout).toBe( - `${JSON.stringify(parsed.error, null, 2)}\n`, - ); - expect(result.json).toMatchObject({ - code: "invalid_request", - category: "invalid_input", - ...(kind === "missing" ? {} : { details: { operation } }), - ...(kind === "inline" - ? {} - : { - input_path: input, - input_reason: - kind === "file" ? "invalid-json" : "read-failed", - }), - }); - expect(result.stderr).toBe(""); - expect(result.exitCode).toBe(1); - }, - ); + for (const kind of ["file", "missing"] as const) + cliTest( + `preserves ${kind} JSON input diagnostics through a real command`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cli-json-status-"); + const input = join(root, "input.json"); + if (kind === "file") await writeFile(input, "{"); + const result = await cli.run({ + arguments: ["compare-javascript-export-shapes", input, "--json"], + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); + + expect(result.json).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + input_path: input, + input_reason: kind === "file" ? "invalid-json" : "read-failed", + ...(kind === "file" + ? { details: { operation: "compare-javascript-export-shapes" } } + : {}), + }); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(1); + }, + ); +}); + +describe("compiled CLI JSON input diagnostic preservation", () => { for (const [format, flags] of [ ["JSON", ["--json"]], ["JSONL", ["--format", "jsonl"]], @@ -113,7 +129,7 @@ describe("compiled CLI JSON input failure status", () => { XDG_CACHE_HOME: root, }, }); - expect(result.json).toMatchObject({ + expect(JSON.parse(result.stdout.trim())).toMatchObject({ code: "invalid_request", input_path: "", input_reason: "read-failed", @@ -121,6 +137,125 @@ describe("compiled CLI JSON input failure status", () => { expect(result.exitCode).toBe(1); }, ); + + cliTest( + "classifies an overlong malformed inline value before dispatch", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cli-json-overlong-"); + const input = `[${"x".repeat(3_000)}`; + const result = await cli.run({ + arguments: ["compare-javascript-export-shapes", input, "--json"], + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); + + expect(result.json).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + operation: "compare-javascript-export-shapes", + issues: [{ path: [], reason: "invalid_format", expected: "JSON" }], + }, + }); + expect(result.json).not.toHaveProperty("input_path"); + expect(result.stdout).not.toContain("read-failed"); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(1); + }, + ); + + cliTest( + "preserves explicit overlong JSON path diagnostics", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cli-json-overlong-path-"); + const input = `[${"x".repeat(3_000)}.json`; + const result = await cli.run({ + arguments: ["compare-javascript-export-shapes", input, "--json"], + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); + + expect(result.json).toMatchObject({ + code: "invalid_request", + input_path: input, + input_reason: "read-failed", + }); + expect(result.exitCode).toBe(1); + }, + ); +}); + +describe("compiled CLI JSON path ambiguity", () => { + cliTest( + "classifies malformed inline JSON when a path prefix is a regular file", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cli-json-not-dir-"); + await writeFile(join(root, "[prefix"), "regular file"); + const input = "[prefix/rest"; + const result = await cli.run({ + arguments: ["capture-browser-scenario", input, "--json"], + cwd: root, + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); + + expect(result.json).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + operation: "capture_browser_scenario", + issues: [{ path: [], reason: "invalid_format", expected: "JSON" }], + }, + }); + expect(result.json).not.toHaveProperty("input_path"); + expect(result.stdout).not.toContain("read-failed"); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(1); + }, + ); + + cliTest.runIf(process.platform !== "win32" && process.getuid?.() !== 0)( + "preserves permission errors for malformed path-like JSON input", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cli-json-denied-"); + const denied = join(root, "[locked"); + const input = "[locked/rest"; + await mkdir(denied); + await chmod(denied, 0); + try { + await expect(readFile(join(root, input))).rejects.toMatchObject({ + code: "EACCES", + }); + const result = await cli.run({ + arguments: ["capture-browser-scenario", input, "--json"], + cwd: root, + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); + + expect(result.json).toMatchObject({ + code: "invalid_request", + input_path: input, + input_reason: "read-failed", + }); + expect(result.exitCode).toBe(1); + } finally { + await chmod(denied, 0o700); + } + }, + ); }); describe("compiled JSON parser and logger success seam", () => { @@ -159,3 +294,78 @@ describe("compiled JSON parser and logger success seam", () => { }, ); }); + +describe("compiled CLI parser with relative brace and bracket paths", () => { + for (const filename of ["[input].json", "{capture}.json"]) + cliTest( + `reads the selected relative file ${filename}`, + async ({ processes }) => { + const root = await createTestTempDirectory("rea-cli-json-path-"); + const value = { selected: filename }; + await writeFile(join(root, filename), JSON.stringify(value)); + const moduleUrl = new URL( + "../../../dist/cliJsonInput.js", + import.meta.url, + ).href; + const result = await processes.run( + process.execPath, + [ + "--input-type=module", + "-e", + `import { parseCliJsonInput } from ${JSON.stringify(moduleUrl)}; + console.log(JSON.stringify(await parseCliJsonInput(process.argv[1], "test-input")));`, + filename, + ], + { + cwd: root, + env: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }, + ); + + expect(result.stdout).toBe(`${JSON.stringify({ ok: true, value })}\n`); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(0); + }, + ); + + for (const filename of ["[missing].json", "{missing}.json"]) + cliTest( + `keeps missing relative path diagnostics for ${filename}`, + async ({ processes }) => { + const root = await createTestTempDirectory("rea-cli-json-missing-"); + const moduleUrl = new URL( + "../../../dist/cliJsonInput.js", + import.meta.url, + ).href; + const result = await processes.run( + process.execPath, + [ + "--input-type=module", + "-e", + `import { parseCliJsonInput } from ${JSON.stringify(moduleUrl)}; + console.log(JSON.stringify(await parseCliJsonInput(process.argv[1], "test-input")));`, + filename, + ], + { + cwd: root, + env: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }, + ); + + expect(JSON.parse(result.stdout.trim())).toMatchObject({ + ok: false, + error: { input_path: filename, input_reason: "read-failed" }, + }); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(0); + }, + ); +}); diff --git a/tests/boundary/cli/jsonOutput.test.ts b/tests/boundary/cli/jsonOutput.test.ts new file mode 100644 index 0000000000..7c1ff4ddd2 --- /dev/null +++ b/tests/boundary/cli/jsonOutput.test.ts @@ -0,0 +1,170 @@ +import { Writable } from "node:stream"; + +import { describe, expect, it } from "vitest"; + +import { writeJsonOutput } from "../../../src/cli/jsonOutput.js"; +import { createStreamedCliJsonOutput } from "../../../src/cli/streamedJsonOutput.js"; + +const collect = async ( + value: unknown, + format: "json" | "jsonl" = "json", +): Promise => { + const chunks: Buffer[] = []; + const destination = new Writable({ + write(chunk: unknown, _encoding, callback) { + if (!Buffer.isBuffer(chunk)) { + callback(new TypeError("Expected a UTF-8 output buffer")); + return; + } + chunks.push(chunk); + callback(); + }, + }); + await writeJsonOutput(value, destination, format); + return Buffer.concat(chunks).toString("utf8"); +}; + +describe("incremental CLI JSON", () => { + it.each([ + null, + true, + -0, + 1e-7, + "", + { empty: [], object: {}, nested: [{ value: null }] }, + { 'quoted"key': '\u0000\b\t\n\f\r\\"', unicode: "雪𝟠\ud800\udfff" }, + { text: `${"x".repeat(8191)}𝟠${"y".repeat(8191)}\ud800` }, + ])("matches native JSON encoding for %j", async (value) => { + expect(await collect(value)).toBe(`${JSON.stringify(value, null, 2)}\n`); + }); + + it("preserves shared objects without interpreting them as cycles", async () => { + const shared = { value: "shared" }; + const value = { copies: [shared, shared] }; + expect(await collect(value)).toBe(`${JSON.stringify(value, null, 2)}\n`); + }); + + it("writes compact JSONL records and preserves an empty record set", async () => { + const value = [{ text: "first\nrecord" }, { text: "雪𝟠" }]; + expect(await collect(value, "jsonl")).toBe( + `${value.map((item) => JSON.stringify(item)).join("\n")}\n`, + ); + expect(await collect([], "jsonl")).toBe(""); + }); + + it("bounds escaped leaf writes and waits for a slow destination", async () => { + const leaf = `\u0000雪𝟠`.repeat(32 * 1024); + const chunks: Buffer[] = []; + let largestChunk = 0; + const destination = new Writable({ + highWaterMark: 1, + write(chunk: unknown, _encoding, callback) { + if (!Buffer.isBuffer(chunk)) { + callback(new TypeError("Expected UTF-8 bytes")); + return; + } + largestChunk = Math.max(largestChunk, chunk.length); + setTimeout(() => { + chunks.push(chunk); + callback(); + }, 1); + }, + }); + await writeJsonOutput({ leaf }, destination); + expect(largestChunk).toBeLessThan(Buffer.byteLength(leaf)); + const actual = Buffer.concat(chunks).toString("utf8"); + const expected = `${JSON.stringify({ leaf }, null, 2)}\n`; + expect(actual.length).toBe(expected.length); + expect(actual === expected).toBe(true); + expect(destination.writableEnded).toBe(false); + }); + + it("waits for the final write even when it fits the destination buffer", async () => { + let completed = false; + const destination = new Writable({ + write(_chunk, _encoding, callback) { + setTimeout(() => { + completed = true; + callback(); + }, 5); + }, + }); + await writeJsonOutput({ result: "small" }, destination); + expect(completed).toBe(true); + }); + + it.each([false, true])( + "propagates a failed destination (deferred: %s)", + async (deferred) => { + const cause = new Error("Fixture destination failed"); + const destination = new Writable({ + write(_chunk, _encoding, callback) { + if (deferred) setTimeout(() => callback(cause), 5); + else callback(cause); + }, + }); + await expect( + writeJsonOutput({ result: "small" }, destination), + ).rejects.toBe(cause); + }, + ); + + it("leaves a reusable destination open without adding error listeners", async () => { + const destination = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + const listeners = destination.listenerCount("error"); + await writeJsonOutput({ result: "first" }, destination); + await writeJsonOutput({ result: "second" }, destination); + expect(destination.listenerCount("error")).toBe(listeners); + expect(destination.writableEnded).toBe(false); + }); + + it("rejects a cycle at the serialization boundary", async () => { + const cycle: Record = {}; + cycle.self = cycle; + await expect(collect(cycle)).rejects.toThrow("circular reference"); + }); + + it.each([undefined, Number.NaN, Number.POSITIVE_INFINITY, 1n, new Date(0)])( + "rejects non-JSON input %s", + async (value) => { + await expect(collect(value)).rejects.toThrow("ordinary JSON"); + }, + ); +}); + +describe("streamed CLI result surface", () => { + it.each([ + [], + ["--format", "md"], + ["--json", "--token-count"], + ["--json", "--token-limit", "5"], + ["--json", "--format", "invalid"], + ])("delegates unsupported output controls %j", (...arguments_) => { + expect(createStreamedCliJsonOutput(arguments_, new Writable())).toBe( + undefined, + ); + }); + + it("keeps ownership after a failed write so no second document is emitted", async () => { + const cause = new Error("Fixture destination failed"); + const destination = new Writable({ + write(_chunk, _encoding, callback) { + callback(cause); + }, + }); + const output = createStreamedCliJsonOutput(["--json"], destination); + if (output === undefined) throw new Error("Missing JSON output surface"); + await expect( + output.write( + { result: "small" }, + { command: "fixture", duration: "1ms", format: "json" }, + ), + ).rejects.toBe(cause); + expect(output.handled).toBe(true); + expect(output.failed).toBe(true); + }); +}); diff --git a/tests/boundary/cli/nativeUiScenarioInputCli.test.ts b/tests/boundary/cli/nativeUiScenarioInputCli.test.ts new file mode 100644 index 0000000000..766b77a796 --- /dev/null +++ b/tests/boundary/cli/nativeUiScenarioInputCli.test.ts @@ -0,0 +1,74 @@ +import { describe, expect } from "vitest"; + +import { cliTest } from "../../support/cli/cliFixture.js"; +import type { TestCli } from "../../support/cli/cliFixture.js"; + +const runCapture = async (cli: TestCli, steps: string | undefined) => { + const arguments_ = [ + "capture-native-ui-scenario", + "/provider-must-not-be-invoked", + "--pid", + "1", + "--window-id", + "1", + ]; + if (steps !== undefined) arguments_.push("--steps", steps); + arguments_.push("--json"); + return cli.run({ + arguments: arguments_, + environment: { REA_LOG_LEVEL: "silent" }, + }); +}; + +describe("native UI scenario CLI input", () => { + cliTest( + "rejects omitted steps under the shared scenario contract", + async ({ cli }) => { + const result = await runCapture(cli, undefined); + + expect(result.exitCode).not.toBe(0); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "capture_native_ui_scenario", + issues: [{ path: ["steps"], reason: "missing_argument" }], + }, + }); + }, + ); + + cliTest("rejects malformed JSON as invalid input", async ({ cli }) => { + const result = await runCapture(cli, "["); + + expect(result.exitCode).not.toBe(0); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "capture_native_ui_scenario", + issues: [ + { + path: ["steps"], + reason: "invalid_format", + expected: "JSON", + }, + ], + }, + }); + }); + + cliTest( + "requires at least one schema-valid scenario step", + async ({ cli }) => { + const result = await runCapture(cli, "[]"); + + expect(result.exitCode).not.toBe(0); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "capture_native_ui_scenario", + issues: [{ path: ["steps"], reason: "out_of_range", minimum: 1 }], + }, + }); + }, + ); +}); diff --git a/tests/boundary/cli/processCli.test.ts b/tests/boundary/cli/processCli.test.ts index a35b92aeb3..2b99cd2408 100644 --- a/tests/boundary/cli/processCli.test.ts +++ b/tests/boundary/cli/processCli.test.ts @@ -13,9 +13,10 @@ import { captureProcessScenarioFile, compareProcessEvidenceFiles, projectProcessCliError, -} from "../../../src/application/ProcessCli.js"; +} from "../../../src/application/process/ProcessCli.js"; import { createEvidence } from "../../../src/domain/evidence.js"; -import { PROCESS_PROVIDER } from "../../../src/application/ProcessEvidence.js"; +import { PROCESS_PROVIDER } from "../../../src/application/process/ProcessEvidence.js"; +import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js"; const roots: string[] = []; const execFileAsync = promisify(execFile); @@ -120,8 +121,17 @@ describe("process CLI errors", () => { const root = await fixture(); const malformed = join(root, "malformed.json"); const invalid = join(root, "invalid.json"); + const invalidUtf8 = join(root, "invalid-utf8.json"); await writeFile(malformed, "not-json"); await writeFile(invalid, "{}"); + await writeFile( + invalidUtf8, + Buffer.concat([ + Buffer.from('{"executable":"'), + Buffer.from([0xff]), + Buffer.from('"}'), + ]), + ); expect( await captureProcessScenarioFile(join(root, "missing.json")), @@ -137,6 +147,12 @@ describe("process CLI errors", () => { message: "Process input file is not valid JSON. Repair the file, then try again.", }); + expect(await captureProcessScenarioFile(invalidUtf8)).toEqual({ + error: "Process command failed", + category: "invalid_input", + message: + "Process input file is not valid JSON. Repair the file, then try again.", + }); expect(await captureProcessScenarioFile(invalid)).toEqual({ error: "Process command failed", code: "invalid_request", @@ -153,6 +169,66 @@ describe("process CLI errors", () => { }); }); + it("rejects NUL arguments before process launch", async () => { + const root = await fixture(); + const scenario = join(root, "nul-argument.json"); + await writeFile( + scenario, + JSON.stringify({ executable: process.execPath, arguments: ["\0"] }), + ); + + expect(await captureProcessScenarioFile(scenario)).toMatchObject({ + error: "Process command failed", + code: "invalid_request", + category: "invalid_input", + details: { + operation: "capture_process_scenario", + issues: [ + { + path: ["arguments", 0], + reason: "invalid_format", + expected: "regex", + message: + "Values passed to operating-system APIs cannot contain NUL", + }, + ], + }, + }); + }); +}); + +describe("process CLI environment key diagnostics", () => { + it("reports the reserved process environment key constraint", async () => { + const root = await fixture(); + const scenario = join(root, "reserved-environment.json"); + await writeFile( + scenario, + JSON.stringify({ + executable: process.execPath, + environment: { REA_PROCESS_RUN_ID: "caller-value" }, + }), + ); + + expect(await captureProcessScenarioFile(scenario)).toMatchObject({ + error: "Process command failed", + code: "invalid_request", + category: "invalid_input", + details: { + operation: "capture_process_scenario", + issues: [ + { + path: ["environment", "REA_PROCESS_RUN_ID"], + reason: "invalid_format", + expected: "regex", + message: "REA_PROCESS_RUN_ID is reserved by the process adapter", + }, + ], + }, + }); + }); +}); + +describe("process CLI evidence validation", () => { it("captures the minimal executable-and-arguments scenario", async () => { const root = await fixture(); const scenario = join(root, "scenario.json"); @@ -217,11 +293,62 @@ describe("process CLI errors", () => { const malformed = join(root, "malformed-evidence.json"); await writeFile(malformed, "{}"); - expect(await compareProcessEvidenceFiles(malformed, malformed)).toEqual({ + expect( + await compareProcessEvidenceFiles(malformed, malformed), + ).toMatchObject({ error: "Process command failed", category: "invalid_input", - message: - "Capture evidence is malformed. Create new capture evidence, then try again.", + code: "invalid_request", + details: { + operation: "compare_process_captures", + issues: expect.arrayContaining([ + expect.objectContaining({ + path: ["parameters"], + reason: "missing_argument", + }), + ]), + }, }); }); }); + +it( + "reports the JSON depth constraint through the compiled comparison CLI", + async () => { + const root = await fixture(); + const left = join(root, "left.json"); + const right = join(root, "right.json"); + const input = INVESTIGATION_EXAMPLES.compare_process_captures.input; + await writeFile(left, JSON.stringify(input.left)); + await writeFile(right, JSON.stringify(input.right)); + const cli = fileURLToPath( + new URL("../../../scripts/rea.mjs", import.meta.url), + ); + const cliArguments = [ + cli, + "compare-process-captures", + left, + right, + "--json", + ]; + const baseline = await execFileAsync(process.execPath, cliArguments); + expect(JSON.parse(baseline.stdout)).toMatchObject({ + operation: "compare_process_captures", + }); + const nested = '{"nested":'.repeat(10_000) + "1" + "}".repeat(10_000); + await writeFile( + left, + JSON.stringify({ + ...input.left, + parameters: { attack: "depth-placeholder" }, + }).replace('"depth-placeholder"', nested), + ); + await expect( + execFileAsync(process.execPath, cliArguments), + ).rejects.toMatchObject({ + code: 1, + stdout: expect.stringContaining("maximum nesting depth"), + }); + }, + CLI_INTEGRATION_TIMEOUT_MS, +); diff --git a/tests/boundary/cli/productCatalog.test.ts b/tests/boundary/cli/productCatalog.test.ts index f3a483ecf4..6dcc93b66d 100644 --- a/tests/boundary/cli/productCatalog.test.ts +++ b/tests/boundary/cli/productCatalog.test.ts @@ -1,3 +1,5 @@ +import { EVMOLE_PROVIDER_IDENTITY } from "../../../src/evm/EvmoleRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -19,6 +21,10 @@ import { MANAGED_WORKFLOW_PROVIDER, } from "../../../src/application/InvestigationProviders.js"; import { CDP_BROWSER_PROVIDER_IDENTITY } from "../../../src/browser/CdpBrowserProvider.js"; +import { + HAR_CAPTURE_PROVIDER_IDENTITY, + MITMPROXY_CAPTURE_PROVIDER_IDENTITY, +} from "../../../src/browser/history/CaptureRelease.js"; import { CDP_ELECTRON_PROVIDER_IDENTITY } from "../../../src/browser/CdpElectronProvider.js"; import { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY } from "../../../src/browser/PlaywrightElectronActiveProvider.js"; import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js"; @@ -89,7 +95,7 @@ describe("canonical product catalog", () => { ); }); - it("matches every source-derived checked-in product fact", async () => { + it("matches every source-derived build-generated product fact", async () => { const catalog = await createProductCatalog(root); expect(catalog.tools.total).toBe(TOOL_CONTRACTS.length); expect( @@ -106,9 +112,13 @@ describe("canonical product catalog", () => { BINWALK_PROVIDER_IDENTITY, UNBLOB_PROVIDER_IDENTITY, WAKARU_PROVIDER_IDENTITY, + HAR_CAPTURE_PROVIDER_IDENTITY, + MITMPROXY_CAPTURE_PROVIDER_IDENTITY, GHIDRA_PROVIDER_IDENTITY, IDA_PROVIDER_IDENTITY, NATIVE_MACOS_PROVIDER_IDENTITY, + PWNTOOLS_PROVIDER_IDENTITY, + EVMOLE_PROVIDER_IDENTITY, ARTIFACT_GRAPH_PROVIDER, ANDROID_APPLICATION_PROVIDER, APPLE_APPLICATION_PROVIDER, @@ -185,14 +195,19 @@ describe("canonical product catalog", () => { "project_managed_application_graph", "verify_managed_native_boundaries", ]); + // Runtime schema commitments must not fan out into checked-in documentation. + // Provider commitments cover the facts actually present in this projection. + expect(Object.keys(catalog.runtime_catalog.digests)).toEqual([ + "providers_sha256", + ]); expect(catalog.runtime_catalog.digests.providers_sha256).toBe( providerCatalogDigest(catalog.providers), ); expect( - JSON.parse(await readFile("docs/product-catalog.json", "utf8")), + JSON.parse(await readFile("docs/public/product-catalog.json", "utf8")), ).toEqual(catalog); expect(await serializeProductCatalog(catalog)).toBe( - await readFile("docs/product-catalog.json", "utf8"), + await readFile("docs/public/product-catalog.json", "utf8"), ); await expect( assertDocumentationFacts(root, catalog), @@ -286,6 +301,8 @@ describe("canonical product catalog drift", () => { true, ); expect(issues.some((issue) => issue.includes("Future Client"))).toBe(true); + expect(issues).toContain("docs/installation.md: missing Future Client"); + expect(issues).not.toContain("README.md: missing Future Client"); }); it("changes the provider projection digest when provider facts drift", async () => { diff --git a/tests/boundary/cli/scenarioCliInputDiagnostics.test.ts b/tests/boundary/cli/scenarioCliInputDiagnostics.test.ts new file mode 100644 index 0000000000..bc98d9d9f6 --- /dev/null +++ b/tests/boundary/cli/scenarioCliInputDiagnostics.test.ts @@ -0,0 +1,64 @@ +import { describe, expect } from "vitest"; + +import { cliTest } from "../../support/cli/cliFixture.js"; + +describe("scenario CLI schema diagnostics", () => { + cliTest("reports browser scenario schema paths", async ({ cli }) => { + const result = await cli.run({ + arguments: ["capture-browser-scenario", "{}", "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + + expect(result.exitCode).not.toBe(0); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "capture_browser_scenario", + issues: expect.arrayContaining([ + { + path: ["browser"], + reason: "missing_argument", + expected: "object", + }, + { + path: ["start_url"], + reason: "missing_argument", + expected: "object", + }, + { + path: ["actions"], + reason: "missing_argument", + expected: "array", + }, + ]), + }, + }); + }); + + cliTest("reports Electron scenario schema paths", async ({ cli }) => { + const result = await cli.run({ + arguments: ["capture-electron-scenario", "{}", "--json"], + environment: { REA_LOG_LEVEL: "silent" }, + }); + + expect(result.exitCode).not.toBe(0); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "capture_electron_scenario", + issues: expect.arrayContaining([ + { + path: ["executable_path"], + reason: "missing_argument", + expected: "string", + }, + { + path: ["application_path"], + reason: "missing_argument", + expected: "string", + }, + ]), + }, + }); + }); +}); diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts new file mode 100644 index 0000000000..dd0e0fd8ef --- /dev/null +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -0,0 +1,597 @@ +import { createHash } from "node:crypto"; +import { access, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; +import { evmInterfaceSchema } from "../../../src/domain/evm/evmInterface.js"; +import { EvmoleInterfaceProvider } from "../../../src/evm/EvmoleInterfaceProvider.js"; +import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; +import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; +import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; +import { + createTestWorkspace, + removeTestWorkspace, +} from "../../support/workspace/workspaceFixture.js"; + +const unsupportedHost = process.platform !== "linux" || process.arch !== "x64"; +const requestSchema = z.strictObject({ + snapshot_path: z.string(), + reply_path: z.string(), + failure_marker_path: z.string(), + encoding: z.enum(["raw", "hex"]), +}); +const fixture = async () => { + const workspace = await createTestWorkspace("rea-evm-interface-boundary-"); + onTestFinished(() => removeTestWorkspace(workspace.root)); + const path = join(workspace.root, "selected.hex"); + await writeFile(path, "0x60006000f3\n"); + return { path, root: workspace.root }; +}; + +it.runIf(!unsupportedHost).each(["SIGXCPU", "SIGXFSZ", "EFBIG"])( + "preserves %s with configured limits distinct from unknown effective limits", + async (failure) => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new EvmoleInterfaceProvider({}, async (spawn) => { + ownedPath = spawn.cwd ?? ""; + if (failure === "EFBIG") + await writeFile(join(ownedPath, "resource.failure"), "F"); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + failure === "EFBIG" + ? 'process.stdout.write("resource diagnostic", () => { process.exitCode = 76; })' + : failure === "SIGXFSZ" + ? 'process.on("SIGXFSZ", () => { process.removeAllListeners("SIGXFSZ"); process.kill(process.pid, "SIGXFSZ"); }); process.stdout.write("resource diagnostic", () => process.kill(process.pid, "SIGXFSZ")); setTimeout(() => {}, 1000);' + : `process.stdout.write("resource diagnostic", () => process.kill(process.pid, "${failure}"))`, + ], + }); + }); + const result = await provider.inspect({ path, encoding: "hex" }); + if (result.ok) throw new Error("Expected observed resource failure"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "resource_constraint", + details: { + resource: failure === "SIGXCPU" ? "cpu" : "file-size", + reported_limits: { + configured_soft_limits: { cpu_seconds: expect.any(Number) }, + effective_soft_limits: null, + }, + captured_output: { stdout: "resource diagnostic", truncated: false }, + }, + }); + expect(result.error.message).toContain( + failure === "EFBIG" ? "exact write failure cause" : "exact signal cause", + ); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each(["missing", "wrong"])( + "does not classify a launcher exit 76 with %s worker failure evidence", + async (scenario) => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new EvmoleInterfaceProvider({}, async (spawn) => { + ownedPath = spawn.cwd ?? ""; + if (scenario === "wrong") + await writeFile( + join(ownedPath, "resource.failure"), + Buffer.from([0xc6]), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + 'process.stdout.write("launcher failure", () => { process.exitCode = 76; });', + ], + }); + }); + const result = await provider.inspect({ path, encoding: "hex" }); + if (result.ok) throw new Error("Expected launcher failure"); + expect(projectAnalysisError(result.error).code).toBe("execution_failure"); + expect(result.error).toMatchObject({ + diagnostics: { + file_size_failure_marker: { + verified: false, + failure: expect.any(String), + }, + captured_output: { stdout: "launcher failure", truncated: false }, + }, + }); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost)( + "retains actual diagnostic truncation when overflow and process cleanup both fail", + async () => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new EvmoleInterfaceProvider({}, async (spawn) => { + ownedPath = spawn.cwd ?? ""; + const launched = await spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", 'process.stdout.write("x".repeat(1048577))'], + }); + return { + ...launched, + cleanup: async () => { + await launched.cleanup?.(); + throw new Error("injected post-cleanup reporting failure"); + }, + }; + }); + const result = await provider.inspect({ path, encoding: "hex" }); + if (result.ok) throw new Error("Expected compounded lifecycle failure"); + expect(result.error).toMatchObject({ + cleanupIncomplete: true, + diagnostics: { + reason: "injected post-cleanup reporting failure", + previous_error: { failure_kind: "output-limit" }, + captured_output: { truncated: true, stderr: "" }, + }, + }); + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each([1048576, 1048577])( + "keeps complete diagnostics or rejects a valid worker reply with %s diagnostic bytes", + async (size) => { + const { path } = await fixture(); + let ownedPath = ""; + const provider = new EvmoleInterfaceProvider({}, async (spawn) => { + ownedPath = spawn.cwd ?? ""; + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("Request missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + const bytes = Buffer.from("60006000f3", "hex"); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: true, + version: "0.9.3", + bytecode: { + sha256: createHash("sha256").update(bytes).digest("hex"), + bytes: bytes.length, + hex: bytes.toString("hex"), + }, + raw: { functions: [] }, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", `process.stderr.write("x".repeat(${String(size)}))`], + }); + }); + const result = await provider.inspect({ path, encoding: "hex" }); + if (size === 1048576) { + if (!result.ok) throw result.error; + const report = evmInterfaceSchema.parse(result.value.result); + expect(report.diagnostics.stderr).toHaveLength(size); + expect(report.diagnostics.truncated).toBe(false); + } else { + if (result.ok) + throw new Error("Expected explicit diagnostic budget failure"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "unreadable_output", + details: { captured_output: { truncated: true } }, + }); + } + await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it + .runIf(!unsupportedHost) + .each([ + "success", + "format", + "unsupported", + "wrong-version", + "wrong-decoded-carrier", + "broken-reply", + "out-of-range-body", + ])( + "binds source carrier/worker representation and releases owned files: %s", + async (scenario) => { + const { path } = await fixture(); + let ownedRoot = ""; + const provider = new EvmoleInterfaceProvider( + { NODE_OPTIONS: "--inspect=0" }, + async (spawn) => { + ownedRoot = spawn.cwd ?? ""; + expect(spawn.hostEnvironment?.NODE_OPTIONS).toBeUndefined(); + expect(spawn.arguments).toContain("--as=3221225472:"); + expect(spawn.arguments).toContain("--disable-wasm-trap-handler"); + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("request missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + expect(request.encoding).toBe("hex"); + expect(await readFile(request.snapshot_path, "utf8")).toBe( + "0x60006000f3\n", + ); + const hex = scenario === "wrong-decoded-carrier" ? "00" : "60006000f3"; + const bytes = Buffer.from(hex, "hex"); + await writeFile( + request.reply_path, + scenario === "broken-reply" + ? "broken" + : JSON.stringify( + ["format", "unsupported"].includes(scenario) + ? { + ok: false, + reason: scenario, + message: "Malformed hex carrier.", + } + : { + ok: true, + version: scenario === "wrong-version" ? "0.0.0" : "0.9.3", + bytecode: { + sha256: createHash("sha256") + .update(bytes) + .digest("hex"), + bytes: bytes.length, + hex, + }, + raw: { + functions: + scenario === "out-of-range-body" + ? [ + { + selector: "aabbccdd", + bytecodeOffset: 5, + dispatch: "abi", + }, + ] + : [], + }, + }, + ), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", 'process.stderr.write("upstream warning")'], + }); + }, + ); + const result = await provider.inspect({ path, encoding: "hex" }); + if (scenario === "success") { + if (!result.ok) throw result.error; + expect(result.value.result).toMatchObject({ + artifact: { path, encoding: "hex", bytes: 13 }, + bytecode: { hex: "60006000f3", bytes: 5 }, + diagnostics: { stderr: "upstream warning" }, + }); + expect(result.value.rawResult).toEqual({ functions: [] }); + } else + expect(result).toMatchObject({ + ok: false, + error: { + _tag: + scenario === "format" + ? "AnalysisInputError" + : scenario === "unsupported" + ? "AnalysisUnsupportedTargetError" + : "AnalysisOutputError", + }, + }); + if (!result.ok) + expect(projectAnalysisError(result.error)).toMatchObject({ + details: { + captured_output: { + stdout: "", + stderr: "upstream warning", + truncated: false, + }, + }, + }); + expect(await readFile(path, "utf8")).toBe("0x60006000f3\n"); + await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each(["cancelled", "output-limit", "process"])( + "preserves lifecycle failure and independently releases an acquired worker: %s", + async (scenario) => { + const { path } = await fixture(); + const controller = new AbortController(); + let pid: number | undefined; + let ownedRoot = ""; + const provider = new EvmoleInterfaceProvider({}, async (spawn) => { + ownedRoot = spawn.cwd ?? ""; + const launched = await spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: [ + "-e", + scenario === "cancelled" + ? 'process.stdout.write("ready\\n");setInterval(()=>undefined,1000)' + : scenario === "output-limit" + ? 'process.stdout.write("x".repeat(1048577))' + : 'process.stderr.write("worker failed");process.exit(7)', + ], + }); + pid = launched.process.pid; + if (scenario === "cancelled") { + await waitForProviderProcessReady(launched.process); + setImmediate(() => controller.abort()); + } + return launched; + }); + expect( + await provider.inspect( + { path, encoding: "hex" }, + { signal: controller.signal }, + ), + ).toMatchObject({ + ok: false, + error: { + _tag: + scenario === "cancelled" + ? "AnalysisCancelledError" + : scenario === "output-limit" + ? "AnalysisOutputError" + : "ProviderAdapterError", + }, + }); + if (pid === undefined) throw new Error("worker not acquired"); + const acquiredPid = pid; + expect(() => process.kill(acquiredPid, 0)).toThrow(); + await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost)( + "distinguishes unavailable limiter, absent carrier and prelaunch cancellation", + async () => { + const { path } = await fixture(); + expect( + await new EvmoleInterfaceProvider({ + REA_EVM_PRLIMIT_COMMAND: path + ".absent", + }).inspect({ path, encoding: "hex" }), + ).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCapabilityUnavailableError" }, + }); + expect( + await new EvmoleInterfaceProvider({}).inspect({ + path: path + ".absent", + encoding: "raw", + }), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } }); + const controller = new AbortController(); + controller.abort(); + expect( + await new EvmoleInterfaceProvider({}).inspect( + { path, encoding: "raw" }, + { signal: controller.signal }, + ), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + }, +); +it.runIf(unsupportedHost)( + "reports unverified host coverage before acquiring a provider", + async () => { + expect( + await new EvmoleInterfaceProvider({}).inspect({ + path: "/selected.hex", + encoding: "hex", + }), + ).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCapabilityUnavailableError" }, + }); + }, +); + +it.runIf(!unsupportedHost)( + "preserves actual snapshot-write cancellation and root cleanup", + async () => { + const { path } = await fixture(); + const controller = new AbortController(); + let ownedRoot = ""; + const provider = new EvmoleInterfaceProvider( + {}, + () => { + throw new Error("must not launch worker"); + }, + async () => { + const root = await PrivateRuntimeRoot.create({ + prefix: "rea-evm-cancel-", + }); + ownedRoot = root.path; + controller.abort(); + return root; + }, + ); + expect( + await provider.inspect( + { path, encoding: "hex" }, + { signal: controller.signal }, + ), + ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost)( + "reports private storage denial without claiming selected carrier read denial", + async () => { + const { path } = await fixture(); + const provider = new EvmoleInterfaceProvider({}, undefined, () => + Promise.reject( + Object.assign(new Error("private root denied"), { code: "EACCES" }), + ), + ); + expect(await provider.inspect({ path, encoding: "hex" })).toMatchObject({ + ok: false, + error: { + _tag: "ProviderAdapterError", + diagnostics: { phase: "worker", reason: "private root denied" }, + }, + }); + }, +); + +it.runIf(!unsupportedHost)( + "retains original format failure when owned-root cleanup fails", + async () => { + const { path, root } = await fixture(); + const provider = new EvmoleInterfaceProvider( + {}, + async (spawn) => { + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("request missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: false, + reason: "format", + message: "Odd selected hex.", + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", "process.exit(0)"], + }); + }, + () => + Promise.resolve({ + path: root, + close: () => Promise.reject(new Error("root cleanup failed")), + }), + ); + expect(await provider.inspect({ path, encoding: "hex" })).toMatchObject({ + ok: false, + error: { + cleanupIncomplete: true, + cleanupResources: [root], + diagnostics: { + reason: "root cleanup failed", + previous_error: { category: "invalid_input" }, + }, + }, + }); + }, +); + +it.runIf(!unsupportedHost).each(["late-cancellation", "cleanup-failure"])( + "retains captured warnings after a successful worker: %s", + async (scenario) => { + const { path } = await fixture(); + const controller = new AbortController(); + let ownedRoot = ""; + const provider = new EvmoleInterfaceProvider( + {}, + async (spawn) => { + const requestPath = spawn.arguments.at(-1); + if (requestPath === undefined) throw new Error("request missing"); + const request = requestSchema.parse( + JSON.parse(await readFile(requestPath, "utf8")), + ); + const bytes = Buffer.from("60006000f3", "hex"); + await writeFile( + request.reply_path, + JSON.stringify({ + ok: true, + version: "0.9.3", + bytecode: { + sha256: createHash("sha256").update(bytes).digest("hex"), + bytes: bytes.length, + hex: bytes.toString("hex"), + }, + raw: { functions: [] }, + }), + ); + return spawnOwnedProviderProcess({ + ...spawn, + command: process.execPath, + arguments: ["-e", 'process.stderr.write("upstream warning")'], + }); + }, + async () => { + const root = await PrivateRuntimeRoot.create({ + prefix: "rea-evm-late-", + }); + ownedRoot = root.path; + return { + path: root.path, + close: async () => { + await root.close(); + if (scenario === "late-cancellation") controller.abort(); + else throw new Error("reported cleanup failure"); + }, + }; + }, + ); + const result = await provider.inspect( + { path, encoding: "hex" }, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("Expected lifecycle failure"); + const output = { stdout: "", stderr: "upstream warning", truncated: false }; + expect(projectAnalysisError(result.error)).toMatchObject({ + category: + scenario === "late-cancellation" ? "cancelled" : "execution_failure", + details: + scenario === "late-cancellation" + ? { captured_output: output } + : { diagnostics: { captured_output: output } }, + }); + await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" }); + }, +); + +it.runIf(!unsupportedHost).each(["directory", "missing-interpreter"])( + "identifies configured limiter constraints without blaming the carrier: %s", + async (scenario) => { + const { path, root } = await fixture(); + const limiter = + scenario === "directory" ? root : join(root, "broken-limiter"); + if (scenario === "missing-interpreter") + await writeFile(limiter, "#!/rea-missing-limiter-interpreter\n", { + mode: 0o700, + }); + let ownedRoot: string | undefined; + const provider = new EvmoleInterfaceProvider( + { REA_EVM_PRLIMIT_COMMAND: limiter }, + undefined, + async () => { + const runtime = await PrivateRuntimeRoot.create({ + prefix: "rea-evm-limiter-", + }); + ownedRoot = runtime.path; + return runtime; + }, + ); + const result = await provider.inspect({ path, encoding: "hex" }); + if (result.ok) throw new Error("Expected unavailable limiter"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "capability_unavailable", + category: "unsupported_provider", + details: { provider_id: "evmole-interface" }, + }); + expect(result.error.message).toContain(limiter); + if (ownedRoot !== undefined) + await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(path, "utf8")).toBe("0x60006000f3\n"); + }, +); diff --git a/tests/boundary/filesystem/artifactComparisonSummary.test.ts b/tests/boundary/filesystem/artifactComparisonSummary.test.ts index 63266bc0bb..7879f96aac 100644 --- a/tests/boundary/filesystem/artifactComparisonSummary.test.ts +++ b/tests/boundary/filesystem/artifactComparisonSummary.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; @@ -31,10 +31,7 @@ const observe = async (path: string) => { describe("artifact comparison summary", () => { it.each([ { removed: 1, added: 1 }, - { removed: 1, added: 2 }, - { removed: 2, added: 1 }, { removed: 0, added: 1 }, - { removed: 1, added: 0 }, { removed: 0, added: 0 }, ])( "counts unchanged paths with $removed removals and $added additions", @@ -74,15 +71,6 @@ describe("artifact comparison summary", () => { contradiction: 0, }); expect(comparison.changes).toHaveLength(removed + added + changed); - expect(comparison.summary.unchanged + comparison.changes.length).toBe( - 3 + removed + added, - ); - expect( - comparison.changes.map(({ logical_path }) => logical_path), - ).not.toContain("same.txt"); - expect( - comparison.changes.map(({ logical_path }) => logical_path), - ).not.toContain("duplicate.txt"); }, ); }); diff --git a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts index bcd1542c47..e87794869b 100644 --- a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts +++ b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts @@ -5,7 +5,7 @@ import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { describe, expect, it } from "vitest"; import { canonicalDigest } from "../../../src/domain/comparisonSemantics.js"; -import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; import type { ArtifactInventoryResult } from "../../../src/domain/artifactGraph.js"; import { createEvidence } from "../../../src/domain/evidence.js"; diff --git a/tests/boundary/filesystem/artifactExtractionNestedIntegrity.test.ts b/tests/boundary/filesystem/artifactExtractionNestedIntegrity.test.ts index f4df985f68..f4925d58aa 100644 --- a/tests/boundary/filesystem/artifactExtractionNestedIntegrity.test.ts +++ b/tests/boundary/filesystem/artifactExtractionNestedIntegrity.test.ts @@ -1,10 +1,11 @@ +import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { createPackage } from "@electron/asar"; import { expect, it } from "vitest"; -import { extractArtifact } from "../../../src/application/ArtifactExtraction.js"; +import { extractArtifact } from "../../../src/artifacts/extraction/ArtifactExtraction.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; it("does not claim unmaterialized nested integrity contradictions were extracted", async () => { @@ -35,7 +36,14 @@ it("does not claim unmaterialized nested integrity contradictions were extracted expect(result.artifacts.map(({ relative_path }) => relative_path)).toEqual([ "app.asar", ]); - expect(result.integrity_contradictions).toEqual([]); + expect(result.integrity_contradictions).toEqual([ + expect.objectContaining({ + logical_path: "app.asar/main.js", + declared_sha256: createHash("sha256").update(original).digest("hex"), + observed_sha256: createHash("sha256").update(changed).digest("hex"), + trust: "observed-untrusted", + }), + ]); expect(result.limitations.join("\n")).toContain( "were not written as their own files", ); diff --git a/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts b/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts index d4532b659a..6d106e3352 100644 --- a/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts +++ b/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { createPackage } from "@electron/asar"; import { describe, expect, it } from "vitest"; -import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { artifactOccurrenceAt, artifactParentPaths, @@ -14,10 +14,6 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const permutations = [ ["pkg/", "pkg/sub/", "pkg/sub/data.txt"], - ["pkg/", "pkg/sub/data.txt", "pkg/sub/"], - ["pkg/sub/", "pkg/", "pkg/sub/data.txt"], - ["pkg/sub/", "pkg/sub/data.txt", "pkg/"], - ["pkg/sub/data.txt", "pkg/", "pkg/sub/"], ["pkg/sub/data.txt", "pkg/sub/", "pkg/"], ]; const expectedParents = { diff --git a/tests/boundary/filesystem/artifactInventorySnapshot.test.ts b/tests/boundary/filesystem/artifactInventorySnapshot.test.ts index 5feec925f6..bd0201d678 100644 --- a/tests/boundary/filesystem/artifactInventorySnapshot.test.ts +++ b/tests/boundary/filesystem/artifactInventorySnapshot.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js"; +import { scanArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js"; describe("artifact inventory snapshot", () => { it("retains the complete scan after the source directory changes", async () => { diff --git a/tests/boundary/filesystem/artifactMzInventory.test.ts b/tests/boundary/filesystem/artifactMzInventory.test.ts index fd9884f957..c3fd44005b 100644 --- a/tests/boundary/filesystem/artifactMzInventory.test.ts +++ b/tests/boundary/filesystem/artifactMzInventory.test.ts @@ -3,15 +3,12 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { classifyArtifactContent } from "../../../src/application/ArtifactGraphConstruction.js"; -import { classifyRoot } from "../../../src/application/ArtifactInventory/classify.js"; +import { classifyArtifactContent } from "../../../src/artifacts/inventory/ArtifactGraphConstruction.js"; +import { classifyRoot } from "../../../src/artifacts/inventory/classify.js"; import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../../../src/artifacts/ArtifactHash.js"; -import { scanCanonicalArtifactInventory } from "../../../src/application/ArtifactInventory/scanCanonical.js"; +import { scanCanonicalArtifactInventory } from "../../../src/artifacts/inventory/scanCanonical.js"; import { targetFormatSchema } from "../../../src/contracts/toolOutputSchemaPrimitives.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; -import { createEvidence } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; -import { identifyRuntimes } from "../../../src/domain/runtimeIdentification.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("MZ artifact classification", () => { @@ -62,7 +59,7 @@ describe("MZ artifact classification", () => { expect(targetFormatSchema.parse("dos-mz")).toBe("dos-mz"); }); - it("keeps standalone and embedded DOS identities through inventory and runtime evidence", async () => { + it("keeps standalone and embedded DOS identities through inventory", async () => { const directory = await createTestTempDirectory("rea-mz-inventory-"); const dosPath = join(directory, "LEGACY.EXE"); const pePath = join(directory, "NATIVE.EXE"); @@ -88,30 +85,6 @@ describe("MZ artifact classification", () => { expect(inventory.nodes.map(({ format }) => format)).toEqual( expect.arrayContaining(["dos-mz", "pe"]), ); - const evidence = createEvidence( - { - path: tree, - sha256: inventory.manifest.root_sha256, - format: "directory", - }, - { id: "inventory-fixture", name: "Inventory fixture", version: null }, - { - operation: "inventory_artifact", - parameters: {}, - result: jsonValueSchema.parse(inventory), - }, - ); - const runtimes = identifyRuntimes({ inventory_evidence: [evidence] }); - expect(runtimes.runtimes).toContainEqual( - expect.objectContaining({ - family: "native", - inspection: "provider-selection-required", - observations: expect.arrayContaining([ - expect.objectContaining({ path: "LEGACY.EXE", format: "dos-mz" }), - expect.objectContaining({ path: "NATIVE.EXE", format: "pe" }), - ]), - }), - ); }); }); diff --git a/tests/boundary/filesystem/artifactZipIntegrity.test.ts b/tests/boundary/filesystem/artifactZipIntegrity.test.ts new file mode 100644 index 0000000000..10b221819c --- /dev/null +++ b/tests/boundary/filesystem/artifactZipIntegrity.test.ts @@ -0,0 +1,113 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; +import { expect, it } from "vitest"; + +import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js"; +import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; +import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; + +it("rejects overlapping ZIP member data ranges through artifact inventory", async () => { + const root = await createTestTempDirectory("rea-zip-overlap-"); + const path = join(root, "overlap.zip"); + await writeOrderedZip(path, ["a.txt", "b.txt"]); + const bytes = await readFile(path); + const centralDirectorySignature = Buffer.from([0x50, 0x4b, 0x01, 0x02]); + const first = bytes.indexOf(centralDirectorySignature); + const second = bytes.indexOf(centralDirectorySignature, first + 4); + if (first < 0 || second <= first) + throw new Error("Expected two ZIP central-directory entries"); + + const firstLocalOffset = bytes.readUInt32LE(first + 42); + const secondLocalOffset = bytes.readUInt32LE(second + 42); + expect(secondLocalOffset).not.toBe(firstLocalOffset); + bytes.writeUInt32LE(firstLocalOffset, second + 42); + await writeFile(path, bytes); + + const target = await parseBinaryTarget(path); + if (!target.ok) throw target.error; + const result = await new ArtifactProvider() + .createClient(target.value) + .execute("inventory_artifact", {}); + + expect(result).toMatchObject({ + ok: false, + error: { _tag: "ArtifactOperationError", reason: "io" }, + }); +}); + +it("preserves encrypted-member and corrupt-CRC observations", async () => { + const root = await createTestTempDirectory("rea-zip-integrity-"); + const encryptedPath = join(root, "encrypted.zip"); + const encryptedWriter = new ZipWriter(new Uint8ArrayWriter()); + await encryptedWriter.add("secret.txt", new TextReader("secret bytes"), { + password: "fixture-password", + }); + await writeFile(encryptedPath, await encryptedWriter.close()); + + const encryptedTarget = await parseBinaryTarget(encryptedPath); + if (!encryptedTarget.ok) throw encryptedTarget.error; + const encryptedResult = await new ArtifactProvider() + .createClient(encryptedTarget.value) + .execute("inventory_artifact", {}); + if (!encryptedResult.ok) throw encryptedResult.error; + const encryptedInventory = artifactInventoryResultSchema.parse( + encryptedResult.value.result, + ); + expect(encryptedInventory.occurrences).toContainEqual( + expect.objectContaining({ + logical_path: "secret.txt", + encrypted: true, + hash_status: "unavailable", + }), + ); + + const corruptPath = join(root, "corrupt-crc.zip"); + const corruptWriter = new ZipWriter(new Uint8ArrayWriter()); + await corruptWriter.add("data.txt", new TextReader("evidence\n")); + await writeFile(corruptPath, await corruptWriter.close()); + const bytes = await readFile(corruptPath); + const localFileHeaderSignature = Buffer.from([0x50, 0x4b, 0x03, 0x04]); + const localHeader = bytes.indexOf(localFileHeaderSignature); + if (localHeader < 0) throw new Error("Expected a ZIP local-file header"); + const dataOffset = + localHeader + + 30 + + bytes.readUInt16LE(localHeader + 26) + + bytes.readUInt16LE(localHeader + 28); + const firstByte = bytes[dataOffset]; + if (firstByte === undefined) throw new Error("Expected ZIP member bytes"); + bytes[dataOffset] = firstByte ^ 1; + await writeFile(corruptPath, bytes); + + const corruptTarget = await parseBinaryTarget(corruptPath); + if (!corruptTarget.ok) throw corruptTarget.error; + const corruptResult = await new ArtifactProvider() + .createClient(corruptTarget.value) + .execute("inventory_artifact", {}); + expect(corruptResult).toMatchObject({ + ok: false, + error: { _tag: "ArtifactOperationError", reason: "io" }, + }); +}); + +it("rejects a truncated ZIP through artifact inventory", async () => { + const root = await createTestTempDirectory("rea-zip-malformed-"); + const path = join(root, "truncated.zip"); + await writeFile(path, Buffer.from([0x50, 0x4b, 0x03, 0x04])); + + const target = await parseBinaryTarget(path); + if (!target.ok) throw target.error; + const result = await new ArtifactProvider() + .createClient(target.value) + .execute("inventory_artifact", {}); + + expect(result).toMatchObject({ + ok: false, + error: { _tag: "ArtifactOperationError", reason: "io" }, + }); +}); diff --git a/tests/boundary/filesystem/asarInventoryContainment.test.ts b/tests/boundary/filesystem/asarInventoryContainment.test.ts index 703fa9f8aa..6e1f021336 100644 --- a/tests/boundary/filesystem/asarInventoryContainment.test.ts +++ b/tests/boundary/filesystem/asarInventoryContainment.test.ts @@ -11,7 +11,7 @@ import { } from "@zip.js/zip.js"; import { describe, expect, it } from "vitest"; -import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { artifactOccurrenceAt, artifactParentPaths, diff --git a/tests/boundary/filesystem/asarInventoryFreshness.test.ts b/tests/boundary/filesystem/asarInventoryFreshness.test.ts index 4d09329351..91cdc81b6e 100644 --- a/tests/boundary/filesystem/asarInventoryFreshness.test.ts +++ b/tests/boundary/filesystem/asarInventoryFreshness.test.ts @@ -5,7 +5,7 @@ import { buffer } from "node:stream/consumers"; import { createPackage, listPackage, uncache } from "@electron/asar"; import { describe, expect, it } from "vitest"; -import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/filesystem/asarStreamingAllocation.test.ts b/tests/boundary/filesystem/asarStreamingAllocation.test.ts new file mode 100644 index 0000000000..630091584d --- /dev/null +++ b/tests/boundary/filesystem/asarStreamingAllocation.test.ts @@ -0,0 +1,474 @@ +import { createHash } from "node:crypto"; +import { fstatSync, type Stats } from "node:fs"; +import { + open as openFile, + lstat, + mkdir, + rm, + readdir, + writeFile, + truncate, +} from "node:fs/promises"; +import { join } from "node:path"; +import { Readable } from "node:stream"; +import { buffer } from "node:stream/consumers"; +import { createPackageWithOptions } from "@electron/asar"; +import { describe, expect, it } from "vitest"; +import { scanArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js"; +import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js"; +import { + closeAsarHandle, + readValidatedAsarEntry, +} from "../../../src/artifacts/AsarEntryStream.js"; +import { hashReadable } from "../../../src/artifacts/ArtifactHash.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const memberBytes = 64 * 1024 * 1024; +const expectedSha256 = (): string => { + const hash = createHash("sha256"); + const zeros = Buffer.alloc(64 * 1024); + for (let written = 0; written < memberBytes; written += zeros.length) + hash.update(zeros); + return hash.digest("hex"); +}; + +const descriptorDirectory = + process.platform === "darwin" ? "/dev/fd" : "/proc/self/fd"; + +const matchingDescriptorCount = async (identity: Stats): Promise => { + const descriptors = await readdir(descriptorDirectory); + return descriptors.filter((descriptor) => { + if (!/^\d+$/u.test(descriptor)) return false; + try { + const observed = fstatSync(Number(descriptor)); + return observed.dev === identity.dev && observed.ino === identity.ino; + } catch { + return false; + } + }).length; +}; + +const waitForNoMatchingDescriptor = async (identity: Stats): Promise => { + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((await matchingDescriptorCount(identity)) === 0) return; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + expect(await matchingDescriptorCount(identity)).toBe(0); +}; + +describe("ASAR entry streaming", () => { + it.skipIf(process.platform === "win32")( + "closes a packed member handle when its unopened output stream is destroyed", + async () => { + const root = await createTestTempDirectory( + "rea-asar-destroy-before-read-", + ); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + await mkdir(source); + await writeFile(join(source, "small.js"), "module.exports = 1;\n"); + await createPackageWithOptions(source, archive, {}); + + const identity = await lstat(archive); + const probe = await openFile(archive, "r"); + expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0); + await probe.close(); + await waitForNoMatchingDescriptor(identity); + const reader = new AsarArtifactReader(archive); + + try { + const entries = []; + for await (const entry of reader.entries()) { + entries.push(entry); + } + const entry = entries.find(({ path }) => path === "small.js"); + if (entry === undefined) throw new Error("Expected packed ASAR member"); + expect(await matchingDescriptorCount(identity)).toBe(0); + const output = await reader.open(entry); + expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0); + const closed = new Promise((resolve) => + output.once("close", resolve), + ); + output.destroy(); + await closed; + await waitForNoMatchingDescriptor(identity); + } finally { + await reader.close(); + } + }, + ); + + it.skipIf(process.platform === "win32")( + "closes a packed member handle when cancellation arrives during open", + async () => { + const root = await createTestTempDirectory("rea-asar-abort-during-stat-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + await mkdir(source); + await writeFile(join(source, "small.js"), "module.exports = 1;\n"); + await createPackageWithOptions(source, archive, {}); + + const identity = await lstat(archive); + const reader = new AsarArtifactReader(archive); + + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries.find(({ path }) => path === "small.js"); + if (entry === undefined) throw new Error("Expected packed ASAR member"); + const controller = new AbortController(); + const opening = reader.open(entry, controller.signal); + controller.abort(); + await expect(opening).rejects.toMatchObject({ reason: "cancelled" }); + await waitForNoMatchingDescriptor(identity); + } finally { + await reader.close(); + } + }, + ); + + it("classifies a missing packed member container as an I/O failure", async () => { + const root = await createTestTempDirectory("rea-asar-missing-member-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + await mkdir(source); + await writeFile(join(source, "small.js"), "module.exports = 1;\n"); + await createPackageWithOptions(source, archive, {}); + + const reader = new AsarArtifactReader(archive); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries.find(({ path }) => path === "small.js"); + if (entry === undefined) throw new Error("Expected packed ASAR member"); + await rm(archive); + await expect(reader.open(entry)).rejects.toMatchObject({ + reason: "io", + cause: { code: "ENOENT" }, + }); + } finally { + await reader.close(); + } + }); +}); + +describe("ASAR entry producer identity", () => { + it.each([false, true] as const)( + "rejects a copied entry with a changed unpacked flag for unpacked=%s", + async (unpacked) => { + const root = await createTestTempDirectory("rea-asar-entry-identity-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + await mkdir(source); + await writeFile(join(source, "small.js"), "module.exports = 1;\n"); + await createPackageWithOptions( + source, + archive, + unpacked ? { unpack: "small.js" } : {}, + ); + const selectedPath = unpacked + ? join(`${archive}.unpacked`, "small.js") + : archive; + const identity = await lstat(selectedPath); + const reader = new AsarArtifactReader(archive); + + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries.find(({ path }) => path === "small.js"); + if (entry === undefined) throw new Error("Expected ASAR file entry"); + expect(entry.unpacked).toBe(unpacked); + expect(await matchingDescriptorCount(identity)).toBe(0); + if (entry.declaredSize === null) + throw new Error("Expected ASAR file size metadata"); + + for (const changedEntry of [ + { ...entry, unpacked: !entry.unpacked }, + { ...entry, path: `stale/${entry.path}` }, + { ...entry, declaredSize: entry.declaredSize + 1 }, + ]) + await expect(reader.open(changedEntry)).rejects.toMatchObject({ + name: "ArtifactReaderFailure", + reason: "integrity", + message: expect.stringContaining( + `ASAR entry metadata changed since inventory: ${entry.path}`, + ), + }); + await waitForNoMatchingDescriptor(identity); + } finally { + await reader.close(); + } + }, + ); + + it.each([ + ["packed to unpacked", false], + ["unpacked to packed", true], + ] as const)( + "rejects a retained %s entry after a new entries pass", + async (_transition, wasUnpacked) => { + const root = await createTestTempDirectory("rea-asar-entry-refresh-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + const memberContents = "module.exports = 1;\n"; + await mkdir(source); + await writeFile(join(source, "small.js"), memberContents); + const reader = new AsarArtifactReader(archive); + + try { + await createPackageWithOptions( + source, + archive, + wasUnpacked ? { unpack: "small.js" } : {}, + ); + const firstEntries = []; + for await (const entry of reader.entries()) firstEntries.push(entry); + const retained = firstEntries.find(({ path }) => path === "small.js"); + if (retained === undefined) + throw new Error("Expected first ASAR file entry"); + const copiedRetained = { + ...retained, + limitations: [...retained.limitations], + }; + + await createPackageWithOptions( + source, + archive, + wasUnpacked ? {} : { unpack: "small.js" }, + ); + const currentEntries = []; + for await (const entry of reader.entries()) currentEntries.push(entry); + const current = currentEntries.find(({ path }) => path === "small.js"); + if (current === undefined) + throw new Error("Expected refreshed ASAR file entry"); + expect(current.unpacked).toBe(!wasUnpacked); + + await expect(reader.open(copiedRetained)).rejects.toMatchObject({ + name: "ArtifactReaderFailure", + reason: "integrity", + message: expect.stringContaining( + "ASAR entry metadata changed since inventory", + ), + }); + expect((await buffer(await reader.open(current))).toString()).toBe( + memberContents, + ); + } finally { + await reader.close(); + } + }, + ); +}); + +describe("ASAR read failures", () => { + it.skipIf(process.platform === "win32")( + "checks cancellation after closing an empty member handle", + async () => { + const root = await createTestTempDirectory("rea-asar-empty-close-"); + const archive = join(root, "container.asar"); + await writeFile(archive, "container bytes"); + const identity = await lstat(archive); + const handle = await openFile(archive, "r"); + try { + expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0); + const controller = new AbortController(); + + await expect( + closeAsarHandle(async () => { + await handle.close(); + controller.abort(); + }, controller.signal), + ).rejects.toMatchObject({ reason: "cancelled" }); + await waitForNoMatchingDescriptor(identity); + } finally { + await handle.close().catch(() => undefined); + } + }, + ); + + it.skipIf(process.platform === "win32")( + "closes the opened unpacked file and retains ASAR context when handle stat fails", + async () => { + const root = await createTestTempDirectory("rea-asar-unpacked-stat-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + await mkdir(source); + await writeFile(join(source, "small.js"), "module.exports = 1;\n"); + await createPackageWithOptions(source, archive, { unpack: "small.js" }); + const unpackedPath = join(`${archive}.unpacked`, "small.js"); + const identity = await lstat(unpackedPath); + const ioError = Object.assign(new Error("device stat failed"), { + code: "EIO", + errno: -5, + syscall: "fstat", + }); + let closeCount = 0; + const reader = new AsarArtifactReader(archive, async (path, flags) => { + const handle = await openFile(path, flags); + expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0); + let statCount = 0; + return { + async stat() { + statCount += 1; + if (statCount === 2) throw ioError; + return handle.stat(); + }, + async close() { + closeCount += 1; + await handle.close(); + }, + createReadStream: (options) => handle.createReadStream(options), + }; + }); + + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries.find(({ path }) => path === "small.js"); + if (entry === undefined) + throw new Error("Expected unpacked ASAR member"); + + await expect(reader.open(entry)).rejects.toMatchObject({ + name: "ArtifactReaderFailure", + reason: "io", + message: expect.stringContaining( + `Could not read ${entry.path} ASAR at ${archive}: device stat failed`, + ), + cause: { code: "EIO", errno: -5, syscall: "fstat" }, + }); + expect(closeCount).toBe(1); + await waitForNoMatchingDescriptor(identity); + } finally { + await reader.close(); + } + }, + ); + + it.each(["missing", "replaced"] as const)( + "validates the container before returning a zero-length member when it is %s", + async (change) => { + const root = await createTestTempDirectory("rea-asar-empty-member-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + await mkdir(source); + await writeFile(join(source, "empty.js"), ""); + await createPackageWithOptions(source, archive, {}); + const reader = new AsarArtifactReader(archive); + + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries.find(({ path }) => path === "empty.js"); + if (entry === undefined) throw new Error("Expected empty ASAR member"); + if (change === "missing") await rm(archive); + else await writeFile(archive, "replacement container"); + + await expect(reader.open(entry)).rejects.toMatchObject( + change === "missing" + ? { reason: "io", cause: { code: "ENOENT" } } + : { reason: "integrity" }, + ); + } finally { + await reader.close(); + } + }, + ); + + it("preserves filesystem errors raised while consuming a member stream", async () => { + const ioError = Object.assign(new Error("device read failed"), { + code: "EIO", + errno: -5, + syscall: "read", + }); + const source = Readable.from( + (async function* () { + yield Buffer.from("partial"); + throw ioError; + })(), + ); + const output = readValidatedAsarEntry( + source, + undefined, + "nested/member.bin", + "/tmp/source.asar", + ); + + await expect(async () => { + for await (const _chunk of output) { + // Consume through the source failure. + } + }).rejects.toMatchObject({ + name: "ArtifactReaderFailure", + reason: "io", + message: expect.stringContaining( + "Could not read nested/member.bin ASAR at /tmp/source.asar", + ), + cause: { code: "EIO", errno: -5, syscall: "read" }, + }); + }); +}); + +describe("ASAR entry streaming", () => { + it("reports the observed digest when an unpacked member changes size", async () => { + const root = await createTestTempDirectory( + "rea-asar-unpacked-size-change-", + ); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + const original = join(source, "small.js"); + const changed = "changed();\n"; + await mkdir(source); + await writeFile(original, "module.exports = 1;\n"); + await createPackageWithOptions(source, archive, { unpack: "small.js" }); + await writeFile(join(`${archive}.unpacked`, "small.js"), changed); + + await expect(scanArtifactInventory(archive)).rejects.toMatchObject({ + reason: "integrity", + details: { + logicalPath: "small.js", + calculatedSha256: createHash("sha256").update(changed).digest("hex"), + unpacked: true, + }, + }); + }); + + it.each([ + ["packed", {}], + ["unpacked", { unpack: "large.bin" }], + ] as const)( + "opens a %s member without allocating its full contents", + async (_kind, options) => { + const root = await createTestTempDirectory("rea-asar-streaming-"); + const source = join(root, "source"); + const archive = join(root, "fixture.asar"); + const member = join(source, "large.bin"); + await mkdir(source); + await writeFile(member, Buffer.alloc(0)); + await truncate(member, memberBytes); + await createPackageWithOptions(source, archive, options); + + const reader = new AsarArtifactReader(archive); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries.find(({ path }) => path === "large.bin"); + if (entry === undefined) throw new Error("Expected large ASAR member"); + + const before = process.memoryUsage().arrayBuffers; + const stream = await reader.open(entry); + const afterOpen = process.memoryUsage().arrayBuffers; + const observed = await hashReadable(stream); + const allocation = afterOpen - before; + + expect(allocation).toBeLessThan(memberBytes / 2); + expect(observed).toMatchObject({ + sha256: expectedSha256(), + bytes: memberBytes, + }); + } finally { + await reader.close(); + } + }, + 120_000, + ); +}); diff --git a/tests/boundary/filesystem/authorizedArtifactInventory.test.ts b/tests/boundary/filesystem/authorizedArtifactInventory.test.ts deleted file mode 100644 index 2598a83a67..0000000000 --- a/tests/boundary/filesystem/authorizedArtifactInventory.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { describe, expect, it } from "vitest"; - -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import { scanAuthorizedArtifactInventory } from "../../../src/application/AuthorizedArtifactInventory.js"; - -describe("authorized artifact inventory", () => { - it("scans a local artifact without configured input roots", async () => { - const root = await createTestTempDirectory("rea-artifact-roots-"); - const path = join(root, "artifact.js"); - await writeFile(path, "export const value = 1;\n"); - const inventory = await scanAuthorizedArtifactInventory(path); - expect(inventory.manifest.root_format).toBe("javascript-bundle"); - }); -}); diff --git a/tests/boundary/filesystem/boundedFileBytes.test.ts b/tests/boundary/filesystem/boundedFileBytes.test.ts index b128804b27..a0c358ae6d 100644 --- a/tests/boundary/filesystem/boundedFileBytes.test.ts +++ b/tests/boundary/filesystem/boundedFileBytes.test.ts @@ -7,7 +7,7 @@ import { readBoundedFileBytes } from "../../../src/process/BoundedFileBytes.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("bounded file byte reads", () => { - it.each([0, 1, 65_536, 65_537])( + it.each([0, 65_536])( "accepts a file exactly at the %i byte limit", async (limit) => { const root = await createTestTempDirectory("rea-bounded-bytes-"); diff --git a/tests/boundary/filesystem/cliJsonInput.test.ts b/tests/boundary/filesystem/cliJsonInput.test.ts index 315f3ebf49..406f14b667 100644 --- a/tests/boundary/filesystem/cliJsonInput.test.ts +++ b/tests/boundary/filesystem/cliJsonInput.test.ts @@ -7,13 +7,18 @@ import { parseCliJsonInput } from "../../../src/cliJsonInput.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("CLI JSON input", () => { - it("preserves JSON scalars and distinguishes malformed inline text from files", async () => { - for (const value of [null, false, 0, "text", [], {}]) - expect( - await parseCliJsonInput(JSON.stringify(value), "test-input"), - ).toEqual({ ok: true, value }); - expect(await parseCliJsonInput("{", "test-input")).toMatchObject({ + it("distinguishes malformed inline text from files and preserves bracket-prefixed paths", async () => { + expect(await parseCliJsonInput("[", "test-input")).toMatchObject({ ok: false, + error: { + details: { issues: [{ reason: "invalid_format", expected: "JSON" }] }, + }, + }); + expect(await parseCliJsonInput("[", "test-input")).toMatchObject({ + ok: false, + error: { + details: { issues: [{ reason: "invalid_format", expected: "JSON" }] }, + }, }); const root = await createTestTempDirectory("rea-json-input-"); const path = join(root, "input.json"); @@ -22,6 +27,24 @@ describe("CLI JSON input", () => { ok: true, value: { value: 1 }, }); + expect(await parseCliJsonInput("false", "test-input")).toEqual({ + ok: true, + value: false, + }); + const bracketPath = join(root, "[input].json"); + await writeFile(bracketPath, '["preserved"]'); + expect(await parseCliJsonInput(bracketPath, "test-input")).toEqual({ + ok: true, + value: ["preserved"], + }); + for (const missingPath of [ + join(root, "{capture}.json"), + join(root, "[missing].json"), + ]) + expect(await parseCliJsonInput(missingPath, "test-input")).toMatchObject({ + ok: false, + error: { input_path: missingPath, input_reason: "read-failed" }, + }); expect(await parseCliJsonInput(root, "test-input")).toMatchObject({ ok: false, error: { input_reason: "read-failed" }, diff --git a/tests/boundary/filesystem/clientConfigurationBom.test.ts b/tests/boundary/filesystem/clientConfigurationBom.test.ts index 9ca72642d0..73650f9717 100644 --- a/tests/boundary/filesystem/clientConfigurationBom.test.ts +++ b/tests/boundary/filesystem/clientConfigurationBom.test.ts @@ -15,7 +15,6 @@ import { inspectClientConfiguration, } from "../../../src/application/SetupClientConfiguration.js"; import { supportedClients } from "../../../src/application/SupportedClients.js"; -import { PRODUCT_IDENTITY } from "../../../src/identity.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe.each([ @@ -46,68 +45,55 @@ describe.each([ await mkdir(dirname(client.configPath), { recursive: true }); await writeFile(client.configPath, original); const currentCommandPath = join(home, "rea"); - - for (const command of [ - ["npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"], - [currentCommandPath, "mcp"], - ]) { - expect(await inspectClientConfiguration(client, {}, command)).toEqual({ - status: "update", - backupPath: `${client.configPath}.rea.backup`, - }); - expect(await clientConfigurationAligned(client, {}, command)).toBe( - false, - ); - expect(await configureClientConfiguration(client, {}, command)).toEqual( - { - status: "configured", - backupPath: `${client.configPath}.rea.backup`, - }, - ); - const updated = await readFile(client.configPath, "utf8"); - expect( - updated.startsWith(`${bom}// Keep the client preferences.\r\n`), - ).toBe(true); - expect( - parseClientConfiguration(updated, client.format).document, - ).toEqual({ + const command = [currentCommandPath, "mcp"]; + expect(await inspectClientConfiguration(client, {}, command)).toEqual({ + status: "update", + backupPath: `${client.configPath}.rea.backup`, + }); + expect(await clientConfigurationAligned(client, {}, command)).toBe(false); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "configured", + backupPath: `${client.configPath}.rea.backup`, + }); + const updated = await readFile(client.configPath, "utf8"); + expect( + updated.startsWith(`${bom}// Keep the client preferences.\r\n`), + ).toBe(true); + expect(parseClientConfiguration(updated, client.format).document).toEqual( + { label: "left\uFEFFright", [serversKey]: { other: { command: "other" }, rea: clientRegistrationEntry(client.format, command, {}), }, theme: "dark", - }); - expect(await clientConfigurationAligned(client, {}, command)).toBe( - true, - ); - expect(await inspectClientConfiguration(client, {}, command)).toEqual({ - status: "already_current", - }); - expect( - await readClientRegistrationStatuses(home, currentCommandPath, { - platform: "linux", - environment: {}, - }), - ).toEqual([ - { - client: name, - config_path: client.configPath, - state: "aligned", - command, - remediation: null, - }, - ]); - expect(await configureClientConfiguration(client, {}, command)).toEqual( - { - status: "unchanged", - }, - ); - expect(await readFile(client.configPath)).toEqual(Buffer.from(updated)); - expect(await readFile(`${client.configPath}.rea.backup`)).toEqual( - Buffer.from(original), - ); - } + }, + ); + expect(await clientConfigurationAligned(client, {}, command)).toBe(true); + expect(await inspectClientConfiguration(client, {}, command)).toEqual({ + status: "already_current", + }); + expect( + await readClientRegistrationStatuses(home, currentCommandPath, { + platform: "linux", + environment: {}, + }), + ).toEqual([ + { + client: name, + config_path: client.configPath, + state: "aligned", + command, + remediation: null, + }, + ]); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect(await readFile(client.configPath)).toEqual(Buffer.from(updated)); + expect(await readFile(`${client.configPath}.rea.backup`)).toEqual( + Buffer.from(original), + ); }, ); }); diff --git a/tests/boundary/filesystem/clientConfigurationTransactions.test.ts b/tests/boundary/filesystem/clientConfigurationTransactions.test.ts index efdd91e6f3..7f803cad8a 100644 --- a/tests/boundary/filesystem/clientConfigurationTransactions.test.ts +++ b/tests/boundary/filesystem/clientConfigurationTransactions.test.ts @@ -34,25 +34,23 @@ describe("client configuration write failures", () => { }); describe("TOML client configuration comparison", () => { - it.each(["nan", "inf", "-inf"])( - "replaces a registration containing %s and preserves unrelated settings", - async (value) => { - const root = await createTestTempDirectory("rea-client-toml-"); - const configPath = join(root, "config.toml"); - const original = `unrelated = ${value}\n[mcp_servers.rea]\ncommand = "old"\nstartup_timeout_sec = ${value}\n`; - await writeFile(configPath, original); - const client = { name: "codex", format: "toml", configPath } as const; - expect(await configureTomlClient(client, {}, ["rea", "mcp"])).toEqual({ - status: "configured", - backupPath: `${configPath}.rea.backup`, - }); - expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); - expect(await readFile(configPath, "utf8")).toContain( - `unrelated = ${value}`, - ); - expect(await configureTomlClient(client, {}, ["rea", "mcp"])).toEqual({ - status: "unchanged", - }); - }, - ); + it("replaces a registration containing a non-finite TOML float and preserves unrelated settings", async () => { + const value = "-inf"; + const root = await createTestTempDirectory("rea-client-toml-"); + const configPath = join(root, "config.toml"); + const original = `unrelated = ${value}\n[mcp_servers.rea]\ncommand = "old"\nstartup_timeout_sec = ${value}\n`; + await writeFile(configPath, original); + const client = { name: "codex", format: "toml", configPath } as const; + expect(await configureTomlClient(client, {}, ["rea", "mcp"])).toEqual({ + status: "configured", + backupPath: `${configPath}.rea.backup`, + }); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); + expect(await readFile(configPath, "utf8")).toContain( + `unrelated = ${value}`, + ); + expect(await configureTomlClient(client, {}, ["rea", "mcp"])).toEqual({ + status: "unchanged", + }); + }); }); diff --git a/tests/boundary/filesystem/dependencyInstall.test.ts b/tests/boundary/filesystem/dependencyInstall.test.ts index f60dfc37f2..39c771791d 100644 --- a/tests/boundary/filesystem/dependencyInstall.test.ts +++ b/tests/boundary/filesystem/dependencyInstall.test.ts @@ -1,12 +1,20 @@ -import { copyFile, mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { spawnSync } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; +import { + copyFile, + mkdir, + readFile, + readdir, + writeFile, + symlink, +} from "node:fs/promises"; +import { dirname, join, relative } from "node:path"; import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const script = join(process.cwd(), "scripts", "check-dependency-install.mjs"); +const sourceRoot = process.cwd(); const fixture = async ( installedVersion?: string, @@ -88,4 +96,175 @@ describe("dependency install freshness", () => { expect(result.status).toBe(1); expect(result.stderr).toContain("alpha: installed 0.9.0, expected 1.0.0"); }); + + it("checks installed dependencies before restoring a cached build", async () => { + const root = await createBuildCacheFixture(); + const seeded = await runNpm(root, ["run", "build:cached"]); + expect(seeded.status, seeded.stderr).toBe(0); + + const rootLock = asRecord( + JSON.parse(await readFile(join(root, "package-lock.json"), "utf8")), + ); + const lockPackages = asRecord(rootLock?.packages); + const rootPackage = asRecord(lockPackages?.[""]); + const directDependencies = { + ...asRecord(rootPackage?.dependencies), + ...asRecord(rootPackage?.devDependencies), + }; + const dependencyName = Object.keys(directDependencies).sort()[0]; + if (dependencyName === undefined) + throw new Error("fixture has no direct dependencies"); + const dependencyPath = `node_modules/${dependencyName}`; + const expectedDependency = asRecord(lockPackages?.[dependencyPath]); + if (typeof expectedDependency?.version !== "string") + throw new Error( + `fixture is missing the locked ${dependencyName} version`, + ); + const installedLockPath = join(root, "node_modules", ".package-lock.json"); + const installedLock = asRecord( + JSON.parse(await readFile(installedLockPath, "utf8")), + ); + const packages = asRecord(installedLock?.packages); + const installedDependency = asRecord(packages?.[dependencyPath]); + if (installedDependency === undefined) + throw new Error(`fixture is missing installed ${dependencyName}`); + installedDependency.version = "0.0.0"; + await writeFile(installedLockPath, JSON.stringify(installedLock)); + + const blocked = await runNpm(root, ["run", "build:cached"]); + expect(blocked.status).toBe(1); + expect(blocked.stderr).toContain( + `${dependencyName}: installed 0.0.0, expected ${expectedDependency.version}`, + ); + + const cached = await runTurbo(root); + expect(cached.status, cached.stderr).toBe(0); + expect(`${cached.stdout}${cached.stderr}`).toMatch(/cache hit/u); + }, 60_000); }); + +const createBuildCacheFixture = async (): Promise => { + const root = await createTestTempDirectory("rea-build-cache-guard-"); + const packageJson = asRecord( + JSON.parse(await readFile(join(sourceRoot, "package.json"), "utf8")), + ); + const scripts = asRecord(packageJson?.scripts); + if ( + packageJson === undefined || + typeof scripts?.["build:cached"] !== "string" + ) + throw new Error("package is missing its cached build command"); + const paths = [ + ".nvmrc", + "package-lock.json", + "turbo.json", + "tsconfig.json", + "tsconfig.build.json", + "scripts/check-dependency-install.mjs", + "scripts/clean-build-output.mjs", + "scripts/generate-package-metadata.mjs", + "scripts/lib/generated-file.mjs", + "skill-src/reverse-engineer-anything/SKILL.md", + "src/generatedPackageMetadata.ts", + ]; + await Promise.all( + paths.map(async (path) => { + const destination = join(root, path); + await mkdir(dirname(destination), { recursive: true }); + await copyFile(join(sourceRoot, path), destination); + }), + ); + await writeFile( + join(root, "package.json"), + JSON.stringify({ + ...packageJson, + scripts: { + ...scripts, + build: "node scripts/cache-fixture-build.mjs", + }, + }), + ); + await writeFile( + join(root, "scripts/cache-fixture-build.mjs"), + 'import { mkdir, writeFile } from "node:fs/promises"; await mkdir("dist", { recursive: true }); await writeFile("dist/cache-fixture.js", "built");\n', + ); + await createNodeModulesLinks(root); + await writeFile( + join(root, "node_modules", ".package-lock.json"), + await readFile(join(sourceRoot, "node_modules", ".package-lock.json")), + ); + return root; +}; + +const createNodeModulesLinks = async (root: string): Promise => { + const source = join(sourceRoot, "node_modules"); + const destination = join(root, "node_modules"); + await mkdir(destination); + for (const entry of await readdir(source, { withFileTypes: true })) { + if (entry.name === ".package-lock.json") continue; + const sourcePath = join(source, entry.name); + const destinationPath = join(destination, entry.name); + if (entry.name.startsWith("@") && entry.isDirectory()) { + await mkdir(destinationPath); + for (const packageName of await readdir(sourcePath)) + await symlink( + join(sourcePath, packageName), + join(destinationPath, packageName), + process.platform === "win32" ? "junction" : undefined, + ); + } else { + await symlink( + sourcePath, + destinationPath, + process.platform === "win32" ? "junction" : undefined, + ); + } + } +}; + +const runNpm = (cwd: string, arguments_: readonly string[]) => + run("npm", arguments_, cwd); + +const runTurbo = (cwd: string) => + run( + process.execPath, + [ + join(cwd, "node_modules", "turbo", "bin", "turbo"), + "run", + "build", + "--output-logs=full", + ], + cwd, + ); + +const run = ( + command: string, + arguments_: readonly string[], + cwd: string, +): Promise<{ + readonly status: number; + readonly stdout: string; + readonly stderr: string; +}> => + new Promise((resolve, reject) => { + const child = spawn(command, [...arguments_], { + cwd, + env: process.env, + shell: process.platform === "win32", + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk: Buffer) => (stdout += chunk.toString())); + child.stderr.on("data", (chunk: Buffer) => (stderr += chunk.toString())); + child.once("error", reject); + child.once("close", (status) => + resolve({ status: status ?? 1, stdout, stderr }), + ); + }); + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const asRecord = (value: unknown): Record | undefined => + isRecord(value) ? value : undefined; diff --git a/tests/boundary/filesystem/directAnalysisPermissions.test.ts b/tests/boundary/filesystem/directAnalysisPermissions.test.ts index fcdef7e5d6..cc2281938f 100644 --- a/tests/boundary/filesystem/directAnalysisPermissions.test.ts +++ b/tests/boundary/filesystem/directAnalysisPermissions.test.ts @@ -54,7 +54,7 @@ describe("direct analysis snapshot files", () => { }); }); - it("replays a cache hit from the supplied path without root configuration", async () => { + it("does not replay unbound historical Evidence from a supplied snapshot", async () => { const directory = await createTestTempDirectory("rea-direct-snapshot-"); const snapshotPath = join(directory, "analysis.json"); const launcherPath = join(directory, "hopper-launcher"); @@ -97,6 +97,6 @@ describe("direct analysis snapshot files", () => { snapshotPath, }, ), - ).resolves.toEqual(evidence); + ).resolves.not.toEqual(evidence); }); }); diff --git a/tests/boundary/filesystem/dosComTarget.test.ts b/tests/boundary/filesystem/dosComTarget.test.ts index fb21eec724..ce5738b3d7 100644 --- a/tests/boundary/filesystem/dosComTarget.test.ts +++ b/tests/boundary/filesystem/dosComTarget.test.ts @@ -6,36 +6,33 @@ import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("DOS COM admission", () => { - it.each(["fixture.com", "fixture.hop", "fixture.js", "fixture.bin"])( - "requires explicit interpretation for %s", - async (name) => { - const directory = await createTestTempDirectory("rea-com-target-"); - const path = join(directory, name); - const bytes = Buffer.from("b83412c3", "hex"); - await writeFile(path, bytes); - const target = await parseBinaryTarget( + it("requires explicit interpretation even when a DOS COM uses a misleading suffix", async () => { + const directory = await createTestTempDirectory("rea-com-target-"); + const path = join(directory, "fixture.js"); + const bytes = Buffer.from("b83412c3", "hex"); + await writeFile(path, bytes); + const target = await parseBinaryTarget( + path, + directory, + "arm64", + undefined, + "dos-com", + ); + expect(target).toMatchObject({ + ok: true, + value: { path, - directory, - "arm64", - undefined, - "dos-com", - ); - expect(target).toMatchObject({ - ok: true, - value: { - path, - format: "dos-com", - kind: "executable", - architecture: "x86", - availableArchitectures: ["x86"], - sha256: createHash("sha256").update(bytes).digest("hex"), - }, - }); - expect(await readFile(path)).toEqual(bytes); - const detected = await parseBinaryTarget(path); - expect(detected.ok && detected.value.format).not.toBe("dos-com"); - }, - ); + format: "dos-com", + kind: "executable", + architecture: "x86", + availableArchitectures: ["x86"], + sha256: createHash("sha256").update(bytes).digest("hex"), + }, + }); + expect(await readFile(path)).toEqual(bytes); + const detected = await parseBinaryTarget(path); + expect(detected.ok && detected.value.format).not.toBe("dos-com"); + }); it.each([0, 0xff01])("rejects invalid file extent %i", async (length) => { const directory = await createTestTempDirectory("rea-com-target-"); const path = join(directory, "fixture.com"); diff --git a/tests/boundary/filesystem/evidenceBundleFiles.test.ts b/tests/boundary/filesystem/evidenceBundleFiles.test.ts index e19ceee379..efc1cd08f4 100644 --- a/tests/boundary/filesystem/evidenceBundleFiles.test.ts +++ b/tests/boundary/filesystem/evidenceBundleFiles.test.ts @@ -10,6 +10,7 @@ import { writeEvidenceBundle, } from "../../../src/application/EvidenceBundleFiles.js"; import { compareEvidenceBundlesCommand } from "../../../src/application/EvidenceBundleCommands.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { createEvidenceBundle, @@ -116,6 +117,32 @@ describe("evidence bundle filesystem adapter", () => { }); }); + it("reports a missing file or output directory with the selected path", async () => { + const directory = await createTestTempDirectory("rea-evidence-"); + const absent = join(directory, "absent.json"); + const read = await readEvidenceBundle(absent); + expect(read).toMatchObject({ + ok: false, + error: { _tag: "EvidenceFileError", reason: "missing", path: absent }, + }); + if (read.ok) throw new Error("Expected a missing-file failure"); + expect(projectAnalysisError(read.error)).toMatchObject({ + message: expect.stringContaining("does not exist at the selected path"), + details: { operation: "read", reason: "missing", path: absent }, + }); + + const orphan = join(directory, "absent", "bundle.json"); + const written = await writeEvidenceBundle(bundle(), orphan, false); + expect(written).toMatchObject({ + ok: false, + error: { _tag: "EvidenceFileError", reason: "missing", path: orphan }, + }); + if (written.ok) throw new Error("Expected a missing-directory failure"); + expect(projectAnalysisError(written.error).message).toContain( + "output directory does not exist", + ); + }); + it("rejects malformed and tampered input", async () => { const directory = await createTestTempDirectory("rea-evidence-"); const malformed = join(directory, "malformed.json"); @@ -136,7 +163,55 @@ describe("evidence bundle filesystem adapter", () => { ); expect(await readEvidenceBundle(tamperedPath)).toMatchObject({ ok: false, - error: { _tag: "EvidenceIntegrityError" }, + error: { + _tag: "EvidenceIntegrityError", + userMessage: expect.stringContaining( + "Evidence semantic identifier does not match its record", + ), + }, + }); + }); + + it("names the failed bundle constraint for a single record or schema mismatch", async () => { + const directory = await createTestTempDirectory("rea-evidence-"); + const record = bundle().records[0]; + const single = join(directory, "record.json"); + await writeFile(single, JSON.stringify(record)); + expect(await readEvidenceBundle(single)).toMatchObject({ + ok: false, + error: { + _tag: "EvidenceIntegrityError", + userMessage: expect.stringContaining( + `this JSON is one Evidence record (${String(record?.evidence_id)})`, + ), + }, + }); + + const partial = join(directory, "partial.json"); + await writeFile(partial, JSON.stringify({ evidence_id: "typo" })); + const rejected = await readEvidenceBundle(partial); + expect(rejected).toMatchObject({ + ok: false, + error: { + userMessage: expect.stringContaining( + "does not match the bundle schema", + ), + }, + }); + expect(rejected).not.toMatchObject({ + error: { userMessage: expect.stringContaining("one Evidence record") }, + }); + + const mismatched = join(directory, "mismatched.json"); + await writeFile(mismatched, JSON.stringify({ ...bundle(), records: {} })); + expect(await readEvidenceBundle(mismatched)).toMatchObject({ + ok: false, + error: { + _tag: "EvidenceIntegrityError", + userMessage: expect.stringContaining( + "does not match the bundle schema at records:", + ), + }, }); }); }); diff --git a/tests/boundary/filesystem/generatedSkill.test.ts b/tests/boundary/filesystem/generatedSkill.test.ts new file mode 100644 index 0000000000..c5db95c1a0 --- /dev/null +++ b/tests/boundary/filesystem/generatedSkill.test.ts @@ -0,0 +1,125 @@ +import { execFile } from "node:child_process"; +import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const exec = promisify(execFile); +const repositoryRoot = fileURLToPath(new URL("../../..", import.meta.url)); +const authored = + '---\nname: reverse-engineer-anything\nmetadata:\n version: "33"\n---\n\n# Instructions\n\nInspect the caller-selected artifact.\n'; + +async function fixture(eol = "\n") { + const root = await createTestTempDirectory("rea-generated-skill-"); + for (const path of [ + "scripts/generate-skill-metadata.mjs", + "scripts/lib/generated-file.mjs", + ]) { + await mkdir(dirname(join(root, path)), { recursive: true }); + await copyFile(join(repositoryRoot, path), join(root, path)); + } + const sourceRoot = join(root, "skill-src/reverse-engineer-anything"); + const outputRoot = join(root, "skills/reverse-engineer-anything"); + await mkdir(join(sourceRoot, "references"), { recursive: true }); + await mkdir(join(root, "dist")); + await writeFile(join(root, "package.json"), '{"type":"module"}\n'); + await writeFile(join(sourceRoot, "SKILL.md"), authored.replaceAll("\n", eol)); + await writeFile( + join(sourceRoot, "references/guide.md"), + `# Guide${eol}Preserve evidence.${eol}`, + ); + const identity = async (count: number, digest: string) => + writeFile( + join(root, "dist/catalogIdentity.js"), + `export const CATALOG_IDENTITY = ${JSON.stringify({ counts: { mcp_tools: count }, digests: { combined_sha256: digest } })};\n`, + ); + await identity(139, "a".repeat(64)); + const generate = (...args: string[]) => + exec( + process.execPath, + [join(root, "scripts/generate-skill-metadata.mjs"), ...args], + { cwd: root }, + ); + return { sourceRoot, outputRoot, identity, generate }; +} + +it.each(["\n", "\r\n"])( + "generates revision-specific metadata without rewriting authored instructions (%j)", + async (eol) => { + const f = await fixture(eol); + await f.generate(); + const first = await readFile(join(f.outputRoot, "SKILL.md"), "utf8"); + expect(first).not.toContain("catalog_digest:"); + expect(first).toContain("tool_count: 139"); + expect( + first.replace(/^ {2}(?:tool_count|catalog_digest):[^\n]*\n/gmu, ""), + ).toBe(authored); + expect( + await readFile(join(f.outputRoot, "references/guide.md"), "utf8"), + ).toBe("# Guide\nPreserve evidence.\n"); + await f.identity(139, "b".repeat(64)); + await f.generate(); + expect(await readFile(join(f.outputRoot, "SKILL.md"), "utf8")).toBe(first); + await f.identity(140, "b".repeat(64)); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("missing or stale"), + }); + await f.generate(); + expect(await readFile(join(f.outputRoot, "SKILL.md"), "utf8")).toContain( + "tool_count: 140", + ); + expect(await readFile(join(f.sourceRoot, "SKILL.md"), "utf8")).toBe( + authored.replaceAll("\n", eol), + ); + expect( + await readFile(join(f.sourceRoot, "references/guide.md"), "utf8"), + ).toBe(`# Guide${eol}Preserve evidence.${eol}`); + await expect(f.generate("--check")).resolves.toMatchObject({ stderr: "" }); + }, +); + +it("rejects missing, stale, and extra packaged references and removes deleted source files on rebuild", async () => { + const f = await fixture(); + await f.generate(); + const guide = join(f.outputRoot, "references/guide.md"); + await rm(guide); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("guide.md is missing or stale"), + }); + await f.generate(); + await writeFile(guide, "modified reference\n"); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("guide.md is missing or stale"), + }); + await f.generate(); + await writeFile( + join(f.outputRoot, "references/removed.md"), + "obsolete instructions\n", + ); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("file inventory drifted"), + }); + await rm(join(f.sourceRoot, "references/guide.md")); + await f.generate(); + await expect(readFile(guide)).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(join(f.outputRoot, "references/removed.md")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await f.generate("--check"); +}); + +it("rejects catalog-dependent metadata in the authored source", async () => { + const f = await fixture(); + await writeFile( + join(f.sourceRoot, "SKILL.md"), + authored.replace(' version: "33"', ' version: "33"\n tool_count: 139'), + ); + await expect(f.generate()).rejects.toMatchObject({ + stderr: expect.stringContaining( + "Catalog metadata belongs in the generated skill", + ), + }); +}); diff --git a/tests/boundary/filesystem/installScript.test.ts b/tests/boundary/filesystem/installScript.test.ts index f6590e6cb5..57450fee4d 100644 --- a/tests/boundary/filesystem/installScript.test.ts +++ b/tests/boundary/filesystem/installScript.test.ts @@ -30,18 +30,22 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { expect(await readdir(fixture.temporary)).toEqual([]); }); - it.each(["22.19.0", "24.11.0", "26.0.0", "27.0.0"])( + it.each(["22.19.0", "24.11.0", "26.0.0"])( "accepts supported Node %s without installing or replacing it", async (version) => { const fixture = await createFixture(); - const result = await runInstaller(fixture, ["--version", "0.3.0"], { - FAKE_NODE_VERSION: version, - }); + const result = await runInstaller( + fixture, + ["--version", "0.3.0", "--dry-run"], + { + FAKE_NODE_VERSION: version, + }, + ); expect(result.stdout).toContain(`Runtime: Node.js ${version}`); }, ); - it.each(["22.18.9", "23.0.0", "24.10.9", "25.1.0", "26.0.0-rc.1"])( + it.each(["22.18.9", "24.10.9", "23.0.0", "26.0.0-rc.1"])( "rejects unsupported Node %s before invoking npm", async (version) => { const fixture = await createFixture(); @@ -77,15 +81,7 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { }); }); - // Every installer failure must carry a recovery action. AGENTS.md requires - // actionable diagnostics, including for mismatch locations, so a failure that - // only states what went wrong is a regression. - // The installer is the one surface whose output is read directly by a person - // in a terminal, so the exact wording is a real contract and stays pinned. - // Alongside it, each row asserts the two properties that matter - // independently of phrasing: the failure is announced as a REA installation - // failure, and the message carries a recovery action. A copy edit then shows - // up as one reviewable diff instead of silently losing the guidance. + // Keep each distinct failure route while checking its reason and recovery. it.each([ [ "unsupported Node", @@ -158,11 +154,6 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { expect(failure).toBeInstanceOf(Error); const stderr = String((failure as { stderr?: unknown }).stderr ?? ""); expect(stderr).toBe(message); - expect(stderr.startsWith("REA installation failed: ")).toBe(true); - // Actionable guidance must survive any rewording. - expect(stderr).toMatch( - /\b(?:Check|Repair|Reinstall|Install|Retry|then retry|use Node)\b/, - ); expect(await readdir(fixture.temporary)).toEqual([]); }, ); @@ -181,6 +172,111 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { }); }); +describe("native macOS installer", { timeout: 20_000 }, () => { + it("installs on Darwin with an empty prefix argument array", async () => { + const fixture = await createFixture(); + const result = await runInstaller(fixture, ["--version", "0.3.0"], { + FAKE_PLATFORM: "Darwin", + FAKE_NPM_PREFIX: join(fixture.home, "npm global prefix"), + }); + expect(result.stdout).toContain("REA 0.3.0 is installed"); + expect(await readFile(fixture.npmLog, "utf8")).toBe( + "prefix --global\ninstall --global rea-agents@0.3.0\n", + ); + expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n"); + expect(await readdir(fixture.temporary)).toEqual([]); + }); +}); + +describe("installer semantic version parsing", { timeout: 20_000 }, () => { + it.each(["1.2.3-01"])( + "rejects malformed semantic versions before invoking npm: %s", + async (version) => { + const fixture = await createFixture(); + await expect( + runInstaller(fixture, ["--version", version, "--dry-run"]), + ).rejects.toMatchObject({ + stderr: + "REA installation failed: version must be an exact semantic version.\n", + }); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }, + ); + + it("rejects valid build metadata before printing a plan or invoking npm", async () => { + const fixture = await createFixture(); + const failure = await runInstaller(fixture, [ + "--version", + "1.2.3+build.01", + "--dry-run", + ]).then( + () => undefined, + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(Error); + const stderr = getProcessOutput(failure, "stderr"); + expect(stderr).toBe( + "REA installation failed: npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry.\n", + ); + expect(getProcessOutput(failure, "stdout")).not.toContain( + "REA install plan", + ); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("accepts a valid prerelease version without build metadata", async () => { + const fixture = await createFixture(); + const result = await runInstaller(fixture, [ + "--version", + "1.2.3-alpha.0", + "--dry-run", + ]); + expect(result.stdout).toContain("Version: 1.2.3-alpha.0"); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("rejects build metadata in the latest release tag before printing a plan", async () => { + const fixture = await createFixture(); + const failure = await runInstaller(fixture, [], { + FAKE_CURL_BODY: '{"tag_name":"rea-agents-1.2.3+build.01"}', + }).then( + () => undefined, + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(Error); + expect(getProcessOutput(failure, "stderr")).toBe( + "REA installation failed: npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry.\n", + ); + expect(getProcessOutput(failure, "stdout")).not.toContain( + "REA install plan", + ); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("rejects an invalid semantic version from the latest release tag", async () => { + const fixture = await createFixture(); + await expect( + runInstaller(fixture, [], { + FAKE_CURL_BODY: '{"tag_name":"rea-agents-1.2.3-01"}', + }), + ).rejects.toMatchObject({ + stderr: + "REA installation failed: the latest release tag was invalid. Retry later or pass --version VERSION.\n", + }); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }); +}); + interface InstallerFixture { readonly home: string; readonly bin: string; @@ -189,6 +285,20 @@ interface InstallerFixture { readonly reaLog: string; } +const getProcessOutput = ( + failure: unknown, + stream: "stdout" | "stderr", +): string => { + if (!(failure instanceof Error)) return ""; + if (stream === "stdout" && "stdout" in failure) { + return typeof failure.stdout === "string" ? failure.stdout : ""; + } + if (stream === "stderr" && "stderr" in failure) { + return typeof failure.stderr === "string" ? failure.stderr : ""; + } + return ""; +}; + const createFixture = async (): Promise => { const root = await createTestTempDirectory("rea-install-test-"); const home = join(root, "home"); @@ -197,23 +307,25 @@ const createFixture = async (): Promise => { const npmLog = join(root, "npm.log"); const reaLog = join(root, "rea.log"); await Promise.all([mkdir(home), mkdir(bin), mkdir(temporary)]); - await executable( - join(bin, "uname"), - '#!/bin/sh\n[ "$1" = "-m" ] && echo x86_64 || printf \'%s\\n\' "${FAKE_PLATFORM:-Linux}"\n', - ); - await executable( - join(bin, "node"), - `#!/bin/sh + await Promise.all([ + executable( + join(bin, "uname"), + '#!/bin/sh\n[ "$1" = "-m" ] && echo x86_64 || printf \'%s\\n\' "${FAKE_PLATFORM:-Linux}"\n', + ), + executable( + join(bin, "node"), + `#!/bin/sh if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.18.0}"; else exec ${shellQuote(process.execPath)} "$@"; fi `, - ); - await executable( - join(bin, "npm"), - `#!/bin/sh + ), + executable( + join(bin, "npm"), + `#!/bin/sh printf '%s\n' "$*" >> "$FAKE_NPM_LOG" [ "$1" = "prefix" ] && { [ "\${FAKE_NPM_PREFIX_FAIL:-}" = "1" ] && exit 1; printf '%s\n' "$FAKE_NPM_PREFIX"; exit 0; } [ "\${FAKE_NPM_FAIL:-}" = "1" ] && exit 1 prefix="$HOME/.local" +[ "\${FAKE_PLATFORM:-Linux}" = "Darwin" ] && prefix="$FAKE_NPM_PREFIX" while [ "$#" -gt 0 ]; do if [ "$1" = "--prefix" ]; then shift; prefix="$1"; fi shift @@ -223,25 +335,27 @@ mkdir -p "$prefix/bin" cp "$FAKE_REA_SOURCE" "$prefix/bin/rea" chmod +x "$prefix/bin/rea" `, - ); - await executable( - join(bin, "curl"), - `#!/bin/sh + ), + executable( + join(bin, "curl"), + `#!/bin/sh [ "\${FAKE_CURL_FAIL:-}" = "1" ] && exit 1 if [ -n "\${FAKE_CURL_BODY:-}" ]; then printf '%s' "$FAKE_CURL_BODY"; else printf '%s' '{"tag_name":"rea-agents-0.3.0"}'; fi `, - ); - await executable( - join(bin, "rea-source"), - `#!/bin/sh + ), + executable( + join(bin, "rea-source"), + `#!/bin/sh printf '%s\n' "$*" >> "$FAKE_REA_LOG" [ "\${FAKE_REA_VERSION_FAIL:-}" = "1" ] && exit 1 [ "$1" = "--version" ] && printf '%s\n' "\${FAKE_REA_VERSION:-0.3.0}" exit 0 `, - ); - for (const command of ["chmod", "cp", "mkdir", "tr"]) - await symlink(`/usr/bin/${command}`, join(bin, command)); + ), + ...["chmod", "cp", "mkdir", "tr"].map((command) => + symlink(`/usr/bin/${command}`, join(bin, command)), + ), + ]); return { home, bin, temporary, npmLog, reaLog }; }; diff --git a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts index a669842728..eebf6959d3 100644 --- a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts +++ b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts @@ -13,6 +13,57 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const compile = promisify(execFile); +type PlistValue = + | string + | number + | boolean + | Date + | Uint8Array + | PlistValue[] + | { [key: string]: PlistValue } + | null; + +const keyedArchiveHierarchy = ( + nodes: readonly { + readonly id: string; + readonly children: readonly number[]; + }[], +) => { + const nodeClass = 1 + nodes.length * 2; + const arrayClass = nodeClass + 1; + const objects: PlistValue[] = Array.from( + { length: arrayClass + 1 }, + () => null, + ); + objects[0] = "$null"; + for (const [index, node] of nodes.entries()) { + const nodeUid = 1 + index * 2; + objects[nodeUid] = { + $class: { UID: nodeClass }, + objectID: node.id, + children: { UID: nodeUid + 1 }, + }; + objects[nodeUid + 1] = { + $class: { UID: arrayClass }, + "NS.objects": node.children.map((child) => ({ UID: 1 + child * 2 })), + }; + } + objects[nodeClass] = { + $classname: "FixtureHierarchyNode", + $classes: ["FixtureHierarchyNode", "NSObject"], + }; + objects[arrayClass] = { + $classname: "__NSArrayI", + $classes: ["__NSArrayI", "NSArray", "NSObject"], + }; + return buildBinary({ + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: objects, + $top: { root: { UID: 1 } }, + }); +}; + describe("compiled Interface Builder bundle reader", () => { it("reads nib plist archives and reports provenance", async () => { const root = await createTestTempDirectory("rea-ib-test-"); @@ -149,7 +200,118 @@ describe("compiled Interface Builder bundle reader", () => { }, ]); }); +}); +describe("keyed archive hierarchy coverage", () => { + it("projects a deep keyed-archive hierarchy through NSArray wrappers", async () => { + const root = await createTestTempDirectory("rea-ib-test-"); + const bundle = join(root, "Example.app"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(resources, { recursive: true }); + const nodes = Array.from({ length: 40 }, (_, index) => ({ + id: `view-${index}`, + children: index === 39 ? [] : [index + 1], + })); + await writeFile(join(resources, "Deep.nib"), keyedArchiveHierarchy(nodes)); + + const analysis = await analyzeInterfaceBuilderBundle({ + bundlePath: bundle, + targetSha256: "f".repeat(64), + }); + + expect(analysis.documents[0]?.hierarchy_complete).toBe(true); + expect(analysis.graph.truncated).toBe(false); + expect(analysis.graph.coverage).toContainEqual( + expect.objectContaining({ + facet: "hierarchy:Contents/Resources/Deep.nib", + status: "complete", + omitted: 0, + }), + ); + expect( + analysis.graph.edges.filter( + ({ relation, id }) => + relation === "contains" && id.includes(":hierarchy:"), + ), + ).toHaveLength(40); + }); + + it("preserves source order for children in an archived collection", async () => { + const root = await createTestTempDirectory("rea-ib-test-"); + const bundle = join(root, "Example.app"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(resources, { recursive: true }); + await writeFile( + join(resources, "Branching.nib"), + keyedArchiveHierarchy([ + { id: "root", children: [1, 2] }, + { id: "first", children: [] }, + { id: "second", children: [] }, + ]), + ); + + const analysis = await analyzeInterfaceBuilderBundle({ + bundlePath: bundle, + targetSha256: "e".repeat(64), + }); + const archiveIdsByNodeId = new Map( + analysis.graph.nodes.map(({ id, attributes }) => [ + id, + attributes.interface_builder_object_id, + ]), + ); + const rootId = analysis.graph.nodes.find( + ({ attributes }) => attributes.interface_builder_object_id === "root", + )?.id; + const childNames = analysis.graph.edges + .filter( + ({ from, relation, id }) => + from === rootId && + relation === "contains" && + id.includes(":hierarchy:"), + ) + .map(({ to }) => (to === null ? null : archiveIdsByNodeId.get(to))); + expect(childNames).toEqual(["first", "second"]); + }); + + it.skipIf(process.platform !== "darwin")( + "projects a deep NSView archive produced by Foundation NSKeyedArchiver", + async () => { + const root = await createTestTempDirectory("rea-ib-test-"); + const bundle = join(root, "Example.app"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(resources, { recursive: true }); + const archive = join(resources, "Foundation.nib"); + await compile("/usr/bin/xcrun", [ + "swift", + "tests/conformance/native/keyed-archive-hierarchy.swift", + archive, + "40", + ]); + + const analysis = await analyzeInterfaceBuilderBundle({ + bundlePath: bundle, + targetSha256: "d".repeat(64), + }); + expect(analysis.documents[0]?.hierarchy_complete).toBe(true); + expect(analysis.graph.truncated).toBe(false); + expect( + analysis.graph.coverage.find( + ({ facet }) => + facet === "hierarchy:Contents/Resources/Foundation.nib", + ), + ).toMatchObject({ status: "complete", examined: 41, omitted: 0 }); + expect( + analysis.graph.edges.filter( + ({ relation, id }) => + relation === "contains" && id.includes(":hierarchy:"), + ), + ).toHaveLength(41); + }, + ); +}); + +describe("compiled Interface Builder bundle reader cancellation", () => { it("honors cancellation during directory traversal", async () => { const root = await createTestTempDirectory("rea-ib-test-"); const controller = new AbortController(); diff --git a/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts b/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts index 74fb9ab221..1871e59c5e 100644 --- a/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts @@ -1,10 +1,10 @@ -import { mkdir, rm, writeFile } from "node:fs/promises"; +import { lstat, mkdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { createPackage } from "@electron/asar"; import { describe, expect, it } from "vitest"; -import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js"; +import { scanArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { readJavaScriptArtifactFiles } from "../../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js"; import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js"; @@ -86,6 +86,24 @@ describe("JavaScript artifact ingestion content identities", () => { } }); + it("rejects a same-size ASAR replacement against the inventoried root digest", async () => { + const { source, archive } = await fixture(); + const snapshot = await scanArtifactInventory(archive); + const original = await lstat(archive); + await writeFile(join(source, "main.js"), "module.exports = 43;\n"); + await createPackage(source, archive); + expect((await lstat(archive)).size).toBe(original.size); + + const reader = new AsarArtifactReader(archive); + try { + await expect( + readJavaScriptArtifactFiles(reader, snapshot), + ).rejects.toMatchObject({ reason: "integrity" }); + } finally { + await reader.close(); + } + }); + it("preserves typed cancellation when the direct ASAR filesystem stream aborts", async () => { const { archive } = await fixture(); const snapshot = await scanArtifactInventory(archive); diff --git a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts index cfab06c6bb..3df383e7a6 100644 --- a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts @@ -8,7 +8,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { readJavaScriptArtifactFiles } from "../../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; -import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js"; +import { scanArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { type ArtifactEntry, type ArtifactReader, diff --git a/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts b/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts index aab5fb66bd..05e3f8e2d9 100644 --- a/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts +++ b/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts @@ -88,6 +88,21 @@ describe("JavaScript export return-shape comparison", () => { expect(result.coverage.status).toBe("partial"); }); + it("orders multiple changed paths deterministically across Unicode names", async () => { + const graphs = await analyzeSources({ + left: `export default () => ({ type: "item", zeta: 1, alpha: 1, "éclair": 1 });`, + right: `export default () => ({ type: "item", zeta: 2, alpha: 2, "éclair": 2 });`, + }); + const first = compare(...graphs); + const second = compare(...graphs); + + expect(new Set(first.changes.map(({ path }) => path))).toEqual( + new Set(["/alpha", "/zeta", "/éclair"]), + ); + expect(first.changes).toEqual(second.changes); + expect(first.comparison_id).toBe(second.comparison_id); + }); + it("does not pair discriminants that an unknown trailing spread can overwrite", async () => { const [left, right] = await analyzeSources({ left: ` diff --git a/tests/boundary/filesystem/javascriptHtmlEntrypoints.test.ts b/tests/boundary/filesystem/javascriptHtmlEntrypoints.test.ts new file mode 100644 index 0000000000..f25d7fb9e8 --- /dev/null +++ b/tests/boundary/filesystem/javascriptHtmlEntrypoints.test.ts @@ -0,0 +1,87 @@ +import { createHash } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect, it } from "vitest"; + +import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const cases = [ + ['', []], + ['', []], + [``, []], + ["", ["actual.js"]], + ['', ["actual.js"]], + ['', ["actual.js"]], + ["", ["actual.js"]], + ['', ["actual.js"]], + ['', []], + ['', []], + ['', []], + ['', ["actual.js"]], +] as const; + +it.each(cases)( + "projects local loads from actual HTML attributes in %s", + async (html, expected) => { + const root = await createTestTempDirectory("rea-html-entrypoints-"); + await Promise.all([ + writeFile(join(root, "index.html"), html), + writeFile(join(root, "actual.js"), "globalThis.ownedActual = true;"), + writeFile(join(root, "decoy.js"), "globalThis.ownedDecoy = true;"), + ]); + const result = await reconstructJavaScriptArtifact({ input_path: root }); + const loads = result.graph.edges.filter( + ({ relation, properties }) => + relation === "loads" && properties.script_path !== undefined, + ); + expect(loads.map(({ properties }) => properties.resolved_path)).toEqual( + expected, + ); + }, +); + +it("preserves source digest, opening-tag range and decoded local base resolution", async () => { + const root = await createTestTempDirectory("rea-html-entrypoint-evidence-"); + const opening = '`; + await mkdir(join(root, "assets")); + await Promise.all([ + writeFile(join(root, "index.html"), html), + writeFile( + join(root, "assets", "actual.js"), + "globalThis.ownedActual = true;", + ), + ]); + const result = await reconstructJavaScriptArtifact({ input_path: root }); + const loads = result.graph.edges.filter( + ({ relation, properties }) => + relation === "loads" && properties.script_path !== undefined, + ); + expect(loads).toHaveLength(1); + expect(loads[0]).toMatchObject({ + properties: { + script_path: "actual.js", + base_href: "assets/", + resolved_path: "assets/actual.js", + }, + evidence: { + authority: "static-relationship-inference", + artifact: { + available: true, + sha256: createHash("sha256").update(html).digest("hex"), + }, + location: { + available: true, + value: { + kind: "source-range", + source: "index.html", + start: { line: 2, column: 0 }, + end: { line: 2, column: opening.length }, + }, + }, + }, + }); +}); diff --git a/tests/boundary/filesystem/javascriptHtmlResourcePaths.test.ts b/tests/boundary/filesystem/javascriptHtmlResourcePaths.test.ts new file mode 100644 index 0000000000..1edd18a866 --- /dev/null +++ b/tests/boundary/filesystem/javascriptHtmlResourcePaths.test.ts @@ -0,0 +1,133 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, join, relative, sep } from "node:path"; + +import { expect, it } from "vitest"; + +import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const cases: readonly { + name: string; + html: string; + files: Readonly>; + expected: readonly string[]; +}[] = [ + { + name: "extension fallback", + html: '', + files: { "app.js": "globalThis.owned = true;" }, + expected: [], + }, + { + name: "directory index", + html: '', + files: { "app/index.js": "globalThis.owned = true;" }, + expected: [], + }, + { + name: "directory package", + html: '', + files: { + "app/package.json": '{"main":"entry.js"}', + "app/entry.js": "globalThis.owned = true;", + }, + expected: [], + }, + { + name: "base directory", + html: '', + files: { "assets/app.js": "globalThis.owned = true;" }, + expected: [], + }, + { + name: "exact base resource", + html: '', + files: { + "assets/app.js": "globalThis.owned = true;", + "app.js": "globalThis.decoy = true;", + }, + expected: ["assets/app.js"], + }, + { + name: "exact script resource", + html: '', + files: { "app.js": "globalThis.owned = true;" }, + expected: ["app.js"], + }, +]; + +it.each(cases)( + "uses exact HTML resource identity for $name", + async ({ html, files, expected }) => { + const root = await createTestTempDirectory("rea-html-resources-"); + await Promise.all( + Object.entries({ ...files, "index.html": html }).map( + async ([path, text]) => { + await mkdir(dirname(join(root, path)), { recursive: true }); + await writeFile(join(root, path), text); + }, + ), + ); + const result = await reconstructJavaScriptArtifact({ input_path: root }); + const loads = result.graph.edges.filter( + ({ relation, properties }) => + relation === "loads" && properties.script_path !== undefined, + ); + expect(loads.map(({ properties }) => properties.resolved_path)).toEqual( + expected, + ); + if (expected.length === 0) { + const reference = result.graph.nodes + .flatMap(({ observations }) => observations) + .find( + ({ properties }) => properties.mechanism === "html-script-reference", + ); + expect(reference).toMatchObject({ + properties: { + script_path: "./app", + resolution_context: "html-reference", + resolution_status: "not-found", + resolved_path: null, + }, + evidence: { + authority: "static-relationship-inference", + location: { + available: true, + value: { kind: "source-range", source: "index.html" }, + }, + limitations: expect.arrayContaining([ + expect.stringContaining("exact HTML resource"), + ]), + }, + }); + } + }, +); + +it("retains CommonJS extension resolution against Node", async () => { + const root = await createTestTempDirectory("rea-html-node-control-"); + await Promise.all([ + writeFile( + join(root, "main.cjs"), + 'const value = require("./app"); module.exports = value;', + ), + writeFile(join(root, "app.js"), 'module.exports = "actual";'), + ]); + const expected = relative( + root, + createRequire(join(root, "main.cjs")).resolve("./app"), + ) + .split(sep) + .join("/"); + const result = await reconstructJavaScriptArtifact({ input_path: root }); + const edge = result.graph.edges.find( + ({ properties }) => + properties.module_link_kind === "require" && + properties.specifier === "./app", + ); + expect(edge?.properties).toMatchObject({ + resolution_status: "resolved", + resolved_path: expected, + }); +}); diff --git a/tests/boundary/filesystem/javascriptPackageExportTargets.test.ts b/tests/boundary/filesystem/javascriptPackageExportTargets.test.ts new file mode 100644 index 0000000000..dafd2cba2b --- /dev/null +++ b/tests/boundary/filesystem/javascriptPackageExportTargets.test.ts @@ -0,0 +1,231 @@ +import { execFile } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; + +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const cases: readonly { + name: string; + exports: unknown; + expected: string | null; + nodeError?: string; + rejectedTarget?: string; +}[] = [ + ...[ + "./../outside.cjs", + "./lib/../actual.cjs", + "././actual.cjs", + "./node_modules/actual.cjs", + "./NODE_MODULES/actual.cjs", + "./%2e%2e/outside.cjs", + "./n%6fde_modules/actual.cjs", + "./lib\\..\\actual.cjs", + "../outside.cjs", + "actual.cjs", + "/actual.cjs", + "file:./actual.cjs", + "", + ].map((target) => ({ + name: `invalid target ${JSON.stringify(target)}`, + exports: target, + expected: null, + nodeError: "ERR_INVALID_PACKAGE_TARGET", + rejectedTarget: target, + })), + { + name: "root subpath rejects a parent segment", + exports: { ".": "./../outside.cjs" }, + expected: null, + nodeError: "ERR_INVALID_PACKAGE_TARGET", + rejectedTarget: "./../outside.cjs", + }, + { + name: "active condition rejects instead of selecting default", + exports: { node: "./../outside.cjs", default: "./actual.cjs" }, + expected: null, + nodeError: "ERR_INVALID_PACKAGE_TARGET", + rejectedTarget: "./../outside.cjs", + }, + { + name: "top-level array skips an invalid target", + exports: ["./../outside.cjs", "./actual.cjs"], + expected: "node_modules/fixture/actual.cjs", + }, + { + name: "root array skips encoded invalid segments", + exports: { ".": ["./%2e%2e/outside.cjs", "./actual.cjs"] }, + expected: "node_modules/fixture/actual.cjs", + }, + { + name: "nested arrays skip invalid package-local segments", + exports: { node: [["./n%6fde_modules/actual.cjs", "./actual.cjs"]] }, + expected: "node_modules/fixture/actual.cjs", + }, + { + name: "last invalid target retains its reason", + exports: ["./../outside.cjs", "././actual.cjs"], + expected: null, + nodeError: "ERR_INVALID_PACKAGE_TARGET", + rejectedTarget: "././actual.cjs", + }, + { + name: "null following an invalid target blocks exports", + exports: ["./../outside.cjs", null], + expected: null, + nodeError: "ERR_PACKAGE_PATH_NOT_EXPORTED", + }, + { + name: "an invalid target following null remains invalid", + exports: [null, "./../outside.cjs"], + expected: null, + nodeError: "ERR_INVALID_PACKAGE_TARGET", + rejectedTarget: "./../outside.cjs", + }, + { + name: "inactive invalid condition does not block default", + exports: { browser: "./../outside.cjs", default: "./actual.cjs" }, + expected: "node_modules/fixture/actual.cjs", + }, + ...["./bad%.cjs", "./bad%C3.cjs"].map((target) => ({ + name: `malformed URL ${target} is selected before URL decoding`, + exports: [target, "./actual.cjs"], + expected: null, + nodeError: "URIError", + })), + { + name: "encoded separator is selected before final path refusal", + exports: ["./lib%2Factual.cjs", "./actual.cjs"], + expected: null, + nodeError: "ERR_INVALID_MODULE_SPECIFIER", + }, + { + name: "missing first target does not select a second file", + exports: ["./missing.cjs", "./actual.cjs"], + expected: null, + nodeError: "MODULE_NOT_FOUND", + }, + { + name: "nested directory is valid", + exports: "./lib/actual.cjs", + expected: "node_modules/fixture/lib/actual.cjs", + }, + { + name: "node_modules substring is not a forbidden segment", + exports: "./node_modules.cjs", + expected: "node_modules/fixture/node_modules.cjs", + }, + { + name: "encoded underscore and letters do not borrow a forbidden directory", + exports: ["./%6eode%5fmodules/actual.cjs", "./actual.cjs"], + expected: "node_modules/fixture/actual.cjs", + }, + { + name: "percent encoding is applied once", + exports: "./literal%252e.cjs", + expected: "node_modules/fixture/literal%2e.cjs", + }, +]; + +const nativeResultSchema = z.object({ + value: z.string().nullable(), + code: z.string().nullable(), +}); + +it.each( + (["import", "require"] as const).flatMap((moduleKind) => + cases.map((testCase) => ({ ...testCase, moduleKind })), + ), +)( + "matches Node $moduleKind target selection for $name", + async ({ + moduleKind, + name, + exports, + expected, + nodeError, + rejectedTarget, + }) => { + const root = await createTestTempDirectory("rea-package-target-"); + const mainPath = moduleKind === "import" ? "main.mjs" : "main.cjs"; + const files = { + [mainPath]: + moduleKind === "import" + ? 'import value from "fixture"; export default value;' + : 'module.exports = require("fixture");', + "node_modules/fixture/package.json": JSON.stringify({ exports }), + ...Object.fromEntries( + [ + "node_modules/fixture/actual.cjs", + "node_modules/outside.cjs", + "node_modules/fixture/lib/actual.cjs", + "node_modules/fixture/node_modules/actual.cjs", + "node_modules/fixture/node_modules.cjs", + "node_modules/fixture/literal%2e.cjs", + ].map((path) => [path, `module.exports = ${JSON.stringify(path)};`]), + ), + }; + await Promise.all( + Object.entries(files).map(async ([path, text]) => { + await mkdir(dirname(join(root, path)), { recursive: true }); + await writeFile(join(root, path), text); + }), + ); + const runner = + moduleKind === "import" + ? 'const load = async () => (await import("fixture")).default;' + : 'const load = async () => require("fixture");'; + const native = await promisify(execFile)( + process.execPath, + [ + "--input-type", + moduleKind === "import" ? "module" : "commonjs", + "--eval", + `${runner} load().then(value => console.log(JSON.stringify({value,code:null})), error => console.log(JSON.stringify({value:null,code:error.code ?? error.name})));`, + ], + { cwd: root, timeout: 5_000 }, + ); + const oracle = nativeResultSchema.parse(JSON.parse(native.stdout)); + expect(oracle.value, name).toBe(expected); + if (nodeError !== undefined) + expect(oracle.code).toBe( + moduleKind === "import" && nodeError === "MODULE_NOT_FOUND" + ? "ERR_MODULE_NOT_FOUND" + : nodeError, + ); + const result = await reconstructJavaScriptArtifact({ + input_path: root, + format: "directory", + }); + const edge = result.graph.edges.find( + ({ properties }) => + properties.specifier === "fixture" && + properties.resolution_status !== undefined && + properties.resolution_status !== null, + ); + expect(edge).toBeDefined(); + expect(edge?.properties.resolved_path).toBe(expected); + if (expected !== null) { + expect(edge?.properties.resolution_status).toBe("resolved"); + } else { + expect(edge?.properties.resolution_status).not.toBe("resolved"); + } + if (rejectedTarget !== undefined) { + expect(edge?.properties.resolution_status).toBe("rejected"); + expect(edge?.evidence.limitations.join(" ")).toContain( + JSON.stringify(rejectedTarget), + ); + expect(edge?.evidence.limitations.join(" ")).toContain("exports target"); + expect(edge?.evidence.limitations.join(" ")).not.toContain( + "not valid package JSON", + ); + expect(edge?.evidence.location).toMatchObject({ + value: { kind: "source-range", source: mainPath }, + }); + } + }, +); diff --git a/tests/boundary/filesystem/javascriptPackagePrecedence.test.ts b/tests/boundary/filesystem/javascriptPackagePrecedence.test.ts index 113f92593b..4c730fb73c 100644 --- a/tests/boundary/filesystem/javascriptPackagePrecedence.test.ts +++ b/tests/boundary/filesystem/javascriptPackagePrecedence.test.ts @@ -1,7 +1,11 @@ -import { mkdir, writeFile } from "node:fs/promises"; +import { execFile } from "node:child_process"; +import { mkdir, realpath, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import { dirname, join, relative, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + import { expect, it } from "vitest"; import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; @@ -25,6 +29,26 @@ const cases: readonly { metadata: { main: "actual.cjs" }, index: false, }, + { + name: "top-level exports array", + metadata: { exports: ["./actual.cjs"] }, + index: true, + }, + { + name: "root subpath exports array", + metadata: { exports: { ".": ["./actual.cjs"] } }, + index: true, + }, + { + name: "exports array skips invalid and unmatched entries", + metadata: { exports: [42, { browser: "./browser.js" }, "./actual.cjs"] }, + index: true, + }, + { + name: "nested root exports array", + metadata: { exports: { ".": [[null, { require: "./actual.cjs" }]] } }, + index: true, + }, { name: "index fallback", metadata: {}, index: true }, { name: "missing legacy main fallback", @@ -140,3 +164,108 @@ it.each(cases)( expect(result.graph.coverage.status).toBe("complete"); }, ); + +const runNode = promisify(execFile); + +it.each([ + { + name: "import uses main before bundler module", + kind: "import", + metadata: { main: "actual.cjs", module: "bundler.mjs" }, + }, + { + name: "import ignores a module-only entry", + kind: "import", + metadata: { module: "bundler.mjs" }, + }, + { + name: "require ignores a module-only entry", + kind: "require", + metadata: { module: "bundler.mjs" }, + }, + { + name: "import preserves a main-only entry", + kind: "import", + metadata: { main: "actual.cjs" }, + }, + { + name: "require preserves main before bundler module", + kind: "require", + metadata: { main: "actual.cjs", module: "bundler.mjs" }, + }, + { + name: "import preserves declared exports before legacy entries", + kind: "import", + metadata: { + main: "actual.cjs", + module: "bundler.mjs", + exports: { import: "./exported.mjs", require: "./actual.cjs" }, + }, + }, +] as const)( + "matches the native Node loader when $name", + async ({ kind, metadata }) => { + const root = await createTestTempDirectory("rea-node-package-fields-"); + const entry = kind === "import" ? "main.mjs" : "main.cjs"; + const source = + kind === "import" + ? `import value from "fixture"; console.log(JSON.stringify({value, resolved: import.meta.resolve("fixture")}));` + : `const value = require("fixture"); console.log(JSON.stringify({value, resolved: require.resolve("fixture")}));`; + const files = { + "package.json": JSON.stringify({ + name: "app", + main: entry, + type: kind === "import" ? "module" : "commonjs", + }), + [entry]: source, + "node_modules/fixture/package.json": JSON.stringify({ + name: "fixture", + ...metadata, + }), + "node_modules/fixture/actual.cjs": "module.exports = 'actual.cjs';", + "node_modules/fixture/index.js": "module.exports = 'index.js';", + "node_modules/fixture/bundler.mjs": "export default 'bundler.mjs';", + "node_modules/fixture/exported.mjs": "export default 'exported.mjs';", + }; + await Promise.all( + Object.entries(files).map(async ([path, text]) => { + await mkdir(dirname(join(root, path)), { recursive: true }); + await writeFile(join(root, path), text); + }), + ); + const { stdout } = await runNode(process.execPath, [join(root, entry)], { + cwd: root, + }); + const oracle: unknown = JSON.parse(stdout); + expect(oracle).toMatchObject({ + value: expect.any(String), + resolved: expect.any(String), + }); + if (typeof oracle !== "object" || oracle === null) + throw new Error("Node returned no package identity"); + const resolved: unknown = Reflect.get(oracle, "resolved"); + if (typeof resolved !== "string") + throw new Error("Node returned no resolved path"); + const expected = relative( + await realpath(root), + kind === "import" ? fileURLToPath(resolved) : resolved, + ) + .split(sep) + .join("/"); + expect(Reflect.get(oracle, "value")).toBe(expected.split("/").at(-1)); + const result = await reconstructJavaScriptArtifact({ + input_path: root, + format: "directory", + }); + const edge = result.graph.edges.find( + ({ properties }) => + properties.module_link_kind === kind && + properties.specifier === "fixture", + ); + expect(edge?.properties).toMatchObject({ + resolution_status: "resolved", + resolved_path: expected, + }); + expect(result.graph.coverage.status).toBe("complete"); + }, +); diff --git a/tests/boundary/filesystem/jsonUtf8Input.test.ts b/tests/boundary/filesystem/jsonUtf8Input.test.ts index c5274bd150..5d16c3c04e 100644 --- a/tests/boundary/filesystem/jsonUtf8Input.test.ts +++ b/tests/boundary/filesystem/jsonUtf8Input.test.ts @@ -5,31 +5,28 @@ import { readJsonFile } from "../../../src/application/JsonFiles.js"; import { parseCliJsonInput } from "../../../src/cliJsonInput.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("JSON file UTF-8 byte integrity", () => { - it.each([0x80, 0xe9, 0xff])( - "rejects invalid UTF-8 byte %s through both file input boundaries", - async (invalidByte) => { - const root = await createTestTempDirectory("rea-json-utf8-"); - const path = join(root, "input.json"); - await writeFile( - path, - Buffer.concat([ - Buffer.from('{"message":"'), - Buffer.from([invalidByte]), - Buffer.from('"}'), - ]), - ); - const fileInput = await readJsonFile(path); - const cliInput = await parseCliJsonInput(path, "compare_web_captures"); - expect(fileInput).toMatchObject({ - ok: false, - error: { reason: "invalid-json" }, - }); - expect(cliInput).toMatchObject({ - ok: false, - error: { input_reason: "invalid-json" }, - }); - }, - ); + it("rejects invalid UTF-8 through both file input boundaries", async () => { + const root = await createTestTempDirectory("rea-json-utf8-"); + const path = join(root, "input.json"); + await writeFile( + path, + Buffer.concat([ + Buffer.from('{"message":"'), + Buffer.from([0x80]), + Buffer.from('"}'), + ]), + ); + const fileInput = await readJsonFile(path); + const cliInput = await parseCliJsonInput(path, "compare_web_captures"); + expect(fileInput).toMatchObject({ + ok: false, + error: { reason: "invalid-json" }, + }); + expect(cliInput).toMatchObject({ + ok: false, + error: { input_reason: "invalid-json" }, + }); + }); it("preserves a legitimate UTF-8 replacement character", async () => { const root = await createTestTempDirectory("rea-json-utf8-control-"); const path = join(root, "input.json"); diff --git a/tests/boundary/filesystem/keyedArchiveSelection.test.ts b/tests/boundary/filesystem/keyedArchiveSelection.test.ts new file mode 100644 index 0000000000..28ebed8476 --- /dev/null +++ b/tests/boundary/filesystem/keyedArchiveSelection.test.ts @@ -0,0 +1,99 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { build } from "plist"; +import { expect, it } from "vitest"; + +import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js"; +import { inspectBundleKeyedArchive } from "../../../src/artifacts/apple/KeyedArchiveReader.js"; +import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const archive = build({ + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: ["$null", "value"], + $top: { root: { CF$UID: 1 }, other: { CF$UID: 1 } }, +}); + +const bundle = async () => { + const root = await createTestTempDirectory("rea-keyed-selection-"); + await mkdir(join(root, "Contents", "Resources"), { recursive: true }); + await writeFile( + join(root, "Contents", "Resources", "Model.plist"), + Buffer.from(archive), + ); + return root; +}; + +const inspect = (bundlePath: string, parameters: Record) => + inspectBundleKeyedArchive({ + bundlePath, + targetSha256: "a".repeat(64), + parameters, + }); + +it.each([ + [".", "invalid_format", "relative to the active app bundle"], + ["Contents/../Model.plist", "invalid_format", "canonical relative"], + ["Contents//Model.plist", "invalid_format", "canonical relative"], + ["Contents/Resources", "invalid_value", "selects a directory"], + ["Contents/Resources/Missing.plist", "invalid_value", "No regular file"], +] as const)( + "reports caller-selected archive path %j as invalid input", + async (path, reason, message) => { + const root = await bundle(); + const rejected = await inspect(root, { path }).catch( + (cause: unknown) => cause, + ); + expect(rejected).toBeInstanceOf(AnalysisInputError); + expect(rejected).toMatchObject({ + issues: [ + { + path: ["path"], + reason, + message: expect.stringContaining(message), + }, + ], + }); + }, +); + +it("reports an absent named root with the archive's available roots", async () => { + const root = await bundle(); + await expect( + inspect(root, { path: "Contents/Resources/Model.plist", root: "missing" }), + ).rejects.toMatchObject({ + issues: [ + { + path: ["root"], + reason: "invalid_value", + message: expect.stringContaining("missing"), + expected: ["root", "other"], + }, + ], + }); +}); + +it.each([ + ["plain plist", Buffer.from(build({ plain: true })), "NSKeyedArchiver"], + [ + "compiled NIBArchive", + Buffer.concat([Buffer.from("NIBArchive"), Buffer.alloc(40)]), + "decode_interface_builder", + ], +])( + "keeps the decoder reason for a selected %s", + async (_label, bytes, detail) => { + const root = await bundle(); + await writeFile(join(root, "Contents", "Resources", "Other.nib"), bytes); + const rejected = await inspect(root, { + path: "Contents/Resources/Other.nib", + }).catch((cause: unknown) => cause); + expect(rejected).toBeInstanceOf(ArtifactReaderFailure); + expect(rejected).toMatchObject({ + reason: "format", + message: expect.stringContaining(detail), + }); + }, +); diff --git a/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts b/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts index a1cbaaf281..6a6ca84011 100644 --- a/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts +++ b/tests/boundary/filesystem/referenceSourceTestFilenames.test.ts @@ -11,16 +11,12 @@ it("imports test-suffixed filenames with unchanged paths, hashes, and languages" const root = await createTestTempDirectory("rea-reference-test-filenames-"); const files = [ ["main_test.go", "package main\n", ["source", "test"], "Go"], - ["main_test.py", "assert True\n", ["source", "test"], "Python"], + ["test_helper.py", "assert True\n", ["source", "test"], "Python"], ["parser_spec.rs", "fn checks() {}\n", ["source", "test"], "Rust"], ["Widget_SPEC.TS", "export {};\n", ["source", "test"], "TypeScript"], ["main_test", "test fixture\n", ["test"], null], - ["main_spec", "spec fixture\n", ["test"], null], - ["test_helper.py", "assert True\n", ["source", "test"], "Python"], ["main.test.js", "export {};\n", ["source", "test"], "JavaScript"], - ["main.go", "package main\n", ["source"], "Go"], ["main_test_helper.py", "value = 1\n", ["source"], "Python"], - ["main_specimen.rs", "fn main() {}\n", ["source"], "Rust"], ["main_test.py.bak", "backup\n", ["unknown"], null], ] as const; await Promise.all( diff --git a/tests/boundary/filesystem/releaseCheckpoint.test.ts b/tests/boundary/filesystem/releaseCheckpoint.test.ts new file mode 100644 index 0000000000..00fff50879 --- /dev/null +++ b/tests/boundary/filesystem/releaseCheckpoint.test.ts @@ -0,0 +1,543 @@ +import { execFile } from "node:child_process"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; +import { z } from "zod"; +import { parse } from "yaml"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execFileAsync = promisify(execFile); +const verifier = fileURLToPath( + new URL("../../../scripts/verify-release-checkpoint.mjs", import.meta.url), +); + +async function git( + directory: string, + args: string[], + date = "2026-01-04T00:00:00Z", +) { + const result = await execFileAsync( + "git", + [ + "-c", + "user.name=REA fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "-c", + `core.hooksPath=${join(directory, "no-hooks")}`, + ...args, + ], + { + cwd: directory, + env: { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date }, + }, + ); + return result.stdout.trim(); +} + +async function metadata(directory: string, version: string, notes: string) { + await mkdir(join(directory, "docs"), { recursive: true }); + await mkdir(join(directory, "src"), { recursive: true }); + const files = { + "package.json": { name: "rea-agents", version }, + "package-lock.json": { version, packages: { "": { version } } }, + ".release-please-manifest.json": { ".": version }, + "server.json": { + version, + packages: [{ identifier: "rea-agents", version }], + }, + "release-please-config.json": { + "changelog-sections": [ + { type: "feat", hidden: false }, + { type: "fix", hidden: false }, + { type: "chore", hidden: true }, + ], + }, + }; + for (const [name, value] of Object.entries(files)) { + await writeFile(join(directory, name), JSON.stringify(value)); + } + await writeFile( + join(directory, "src/generatedPackageMetadata.ts"), + `export const METADATA = {\n version: "${version}",\n};\n`, + ); + await writeFile( + join(directory, "CHANGELOG.md"), + `# Changelog\n\n## [${version}]\n\n${notes}\n`, + ); +} + +async function fixture( + subject = "fix(contracts)!: require absolute paths (#948)", + footer = "", + options: { mergePullRequest?: boolean } = {}, +) { + const directory = await createTestTempDirectory("rea-release-ancestry-"); + await git(directory, ["init", "--initial-branch=main"]); + await metadata(directory, "4.0.0", "Initial release"); + await git(directory, ["add", "."]); + await git(directory, ["commit", "-m", "chore: seed"], "2026-01-01T00:00:00Z"); + await git(directory, ["switch", "-c", "release/5.0.0"]); + await metadata(directory, "5.0.0", "Previous release"); + await git(directory, ["add", "."]); + await git( + directory, + ["commit", "-m", "chore: release 5.0.0"], + "2026-01-03T00:00:00Z", + ); + const releasedSha = await git(directory, ["rev-parse", "HEAD"]); + await git(directory, ["tag", "rea-agents-5.0.0"]); + await git(directory, ["switch", "main"]); + if (options.mergePullRequest) { + await git(directory, ["switch", "-c", "change/contracts"]); + } + await writeFile( + join(directory, "behavior.txt"), + "Unreleased mainline behavior\n", + ); + await git(directory, ["add", "."]); + await git( + directory, + ["commit", "-m", subject, "-m", footer], + "2026-01-02T00:00:00Z", + ); + if (options.mergePullRequest) { + await git(directory, ["switch", "main"]); + await git( + directory, + [ + "merge", + "--no-ff", + "change/contracts", + "-m", + "Merge pull request #948 from fixture/absolute-paths", + "-m", + subject, + ], + "2026-01-02T00:00:00Z", + ); + } + const changedSha = await git(directory, ["rev-parse", "HEAD"]); + await git(directory, [ + "merge", + "--no-ff", + "release/5.0.0", + "-m", + "chore: sync published baseline", + ]); + const sourceSha = await git(directory, ["rev-parse", "HEAD"]); + return { directory, sourceSha, changedSha, releasedSha }; +} + +type Fixture = Awaited>; + +async function candidate( + f: Fixture, + version: string, + notes = "### ⚠ BREAKING CHANGES\n\n* MCP callers must use absolute paths; CLI paths remain compatible. See #948.", +) { + await metadata(f.directory, version, notes); + await git(f.directory, ["add", "."]); + await git( + f.directory, + ["commit", "-m", `chore: release ${version}`], + "2026-01-05T00:00:00Z", + ); + return git(f.directory, ["rev-parse", "HEAD"]); +} + +function verify( + f: Pick, + version = "6.0.0", + phase = "prepare", + stage = "candidate", +) { + return execFileAsync( + process.execPath, + [ + verifier, + "--phase", + phase, + "--stage", + stage, + "--release-branch", + `release/${version}`, + "--source-sha", + f.sourceSha, + ], + { cwd: f.directory }, + ); +} + +const reportSchema = z.object({ + expectedVersion: z.string(), + baselineVersion: z.string(), + applicationSha: z.string(), + commits: z.array( + z.object({ + sha: z.string(), + subject: z.string(), + conventionalTitle: z.string().optional(), + breaking: z.boolean(), + }), + ), + missingNotes: z.array( + z.object({ + sha: z.string(), + subject: z.string(), + conventionalTitle: z.string().optional(), + type: z.string().optional(), + breaking: z.boolean(), + }), + ), +}); + +it("retains an unreleased breaking commit hidden by the side-branch chronological cutoff", async () => { + const f = await fixture(); + const walk = ( + await git(f.directory, ["log", "--format=%H", f.sourceSha]) + ).split("\n"); + expect(walk.slice(0, walk.indexOf(f.releasedSha))).not.toContain( + f.changedSha, + ); + await candidate(f, "6.0.0"); + const result = await verify(f); + const report = reportSchema.parse(JSON.parse(result.stdout)); + expect(report).toMatchObject({ + expectedVersion: "6.0.0", + baselineVersion: "5.0.0", + missingNotes: [], + }); + expect(report.commits).toContainEqual({ + sha: f.changedSha, + subject: "fix(contracts)!: require absolute paths (#948)", + breaking: true, + }); +}); + +it("rejects a wrong bot version before publication", async () => { + const f = await fixture(); + await candidate(f, "5.1.0"); + await expect(verify(f)).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "declares 5.1.0; release branch requires 6.0.0", + ), + }); +}); + +it("rejects a requested minor that omits the breaking ancestry", async () => { + const f = await fixture(); + await expect(verify(f, "5.1.0", "prepare", "source")).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("contains unreleased breaking markers"), + }); +}); + +it("rejects a minor when a GitHub merge commit carries its bang marker in the PR title", async () => { + const f = await fixture("fix(contracts)!: require absolute paths", "", { + mergePullRequest: true, + }); + await expect(verify(f, "5.1.0", "prepare", "source")).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("contains unreleased breaking markers"), + }); +}); + +it.each([ + { + subject: "fix(contracts)!: require absolute paths", + version: "6.0.0", + breaking: true, + }, + { subject: "fix: improve diagnostics", version: "5.0.1", breaking: false }, +])( + "recognizes GitHub merge titles and PR note references for $version", + async ({ subject, version, breaking }) => { + const f = await fixture(subject, "", { mergePullRequest: true }); + await candidate( + f, + version, + `${breaking ? "### ⚠ BREAKING CHANGES" : "### Bug Fixes"}\n\nSee #948 for the change and migration.`, + ); + const report = reportSchema.parse( + JSON.parse((await verify(f, version)).stdout), + ); + expect(report.missingNotes).toEqual([]); + expect(report.commits).toContainEqual({ + sha: f.changedSha, + subject: "Merge pull request #948 from fixture/absolute-paths", + conventionalTitle: subject, + breaking, + }); + }, +); + +it("does not treat an ordinary body example as a merge title or breaking declaration", async () => { + const f = await fixture( + "fix: improve diagnostics", + "Example subject:\n\nfix(contracts)!: example only", + ); + await candidate(f, "5.0.1", `### Bug Fixes\n\n${f.changedSha}`); + await expect(verify(f, "5.0.1")).resolves.toMatchObject({ stderr: "" }); +}); + +it("cannot satisfy migration references with notes retained from an older release", async () => { + const f = await fixture(); + await candidate( + f, + "6.0.0", + "### Features\n\nNew tool\n\n## [5.0.0]\n\n### ⚠ BREAKING CHANGES\n\nSee #948.", + ); + await expect(verify(f)).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "migration notes omit unreleased breaking changes", + ), + }); +}); + +it("recognizes a BREAKING CHANGE footer without a bang in the subject", async () => { + const f = await fixture( + "fix(contracts): change path inputs (#948)", + "BREAKING CHANGE: relative MCP paths are rejected", + ); + await expect(verify(f, "5.1.0", "prepare", "source")).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("contains unreleased breaking markers"), + }); +}); + +it.each([ + { subject: "fix: correct supported output", version: "5.0.1" }, + { subject: "feat: add an optional inspection", version: "5.1.0" }, +])( + "accepts compatible $version without a major increment", + async ({ subject, version }) => { + const f = await fixture(subject); + await candidate(f, version, `### Changes\n\n${f.changedSha} ${subject}`); + const report = reportSchema.parse( + JSON.parse((await verify(f, version)).stdout), + ); + expect(report.expectedVersion).toBe(version); + expect(report.commits.every((entry) => !entry.breaking)).toBe(true); + }, +); + +it("accepts an exact prerelease version", async () => { + const f = await fixture(); + await candidate(f, "6.0.0-rc.1"); + await expect(verify(f, "6.0.0-rc.1")).resolves.toMatchObject({ stderr: "" }); +}); + +it.each(["6.01.0", "6.0", "not-a-version", "6.0.0+build"])( + "rejects invalid release version %s", + async (version) => { + const directory = await createTestTempDirectory( + "rea-release-invalid-version-", + ); + await expect( + verify( + { directory, sourceSha: "1".repeat(40) }, + version, + "prepare", + "source", + ), + ).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("must contain an exact SemVer"), + }); + }, +); + +it("validates candidate options before inspecting the working directory", async () => { + const directory = await createTestTempDirectory( + "rea-release-invalid-candidate-", + ); + await git(directory, ["init", "--initial-branch=main"]); + await expect( + verify({ directory, sourceSha: "1".repeat(40) }, "not-a-version"), + ).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("must contain an exact SemVer"), + }); +}); + +it.each([ + "package-lock.json", + "server.json", + "src/generatedPackageMetadata.ts", +])("rejects inconsistent version metadata in %s", async (path) => { + const f = await fixture(); + await candidate(f, "6.0.0"); + await writeFile( + join(f.directory, path), + path.endsWith(".ts") + ? 'export const METADATA = {\n version: "5.1.0",\n};\n' + : JSON.stringify( + path === "package-lock.json" + ? { version: "6.0.0", packages: { "": { version: "5.1.0" } } } + : path === "server.json" + ? { + version: "6.0.0", + packages: [{ identifier: "rea-agents", version: "5.1.0" }], + } + : { package: { version: "5.1.0" } }, + ), + ); + await git(f.directory, ["add", "."]); + await git(f.directory, ["commit", "-m", "chore: inconsistent metadata"]); + await expect(verify(f)).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "declares 5.1.0; release branch requires 6.0.0", + ), + }); +}); + +it("checks a publish merge using its pre-release first parent baseline", async () => { + const f = await fixture(); + const sourceSha = await candidate(f, "6.0.0"); + const report = reportSchema.parse( + JSON.parse( + (await verify({ ...f, sourceSha }, "6.0.0", "publish", "source")).stdout, + ), + ); + expect(report.applicationSha).toBe(f.sourceSha); + expect(report.baselineVersion).toBe("5.0.0"); +}); + +it("reports other omitted entries for review without inventing a breaking change", async () => { + const f = await fixture("fix: improve diagnostics"); + await candidate(f, "5.0.1", "### Bug Fixes\n\nUpdated diagnostics"); + const result = await verify(f, "5.0.1"); + expect(reportSchema.parse(JSON.parse(result.stdout)).missingNotes).toEqual([ + { + sha: f.changedSha, + subject: "fix: improve diagnostics", + type: "fix", + breaking: false, + }, + ]); + expect(result.stderr).toContain("additional unreleased mainline entries"); +}); + +it("reports a missing published tag with recovery advice", async () => { + const f = await fixture(); + await git(f.directory, ["tag", "-d", "rea-agents-5.0.0"]); + await expect(verify(f, "6.0.0", "prepare", "source")).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("Fetch release tags"), + }); +}); + +it("rejects a published baseline outside the selected ancestry", async () => { + const f = await fixture(); + const next = await candidate(f, "6.0.0"); + await git(f.directory, ["tag", "-f", "rea-agents-5.0.0", next]); + await expect(verify(f, "6.0.0", "prepare", "source")).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "Published baseline is outside the checkpoint ancestry", + ), + }); +}); + +it("reports malformed candidate JSON with the exact metadata path", async () => { + const f = await fixture(); + await candidate(f, "6.0.0"); + await writeFile(join(f.directory, "server.json"), "{"); + await git(f.directory, ["add", "."]); + await git(f.directory, ["commit", "-m", "chore: invalid metadata"]); + await expect(verify(f)).rejects.toMatchObject({ + code: 1, + stderr: expect.stringMatching(/server\.json@.*contains invalid JSON/u), + }); +}); + +it("rejects a candidate from a different source even when versions and notes match", async () => { + const f = await fixture(); + await git(f.directory, ["switch", "release/5.0.0"]); + await candidate(f, "6.0.0"); + await expect(verify(f)).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "Candidate is outside the selected checkpoint ancestry", + ), + }); +}); + +it.skipIf(process.platform === "win32").each([true, false])( + "the actual publish preflight admits the next action only for valid metadata (%s)", + async (valid) => { + const f = await fixture(); + const sourceSha = await candidate(f, valid ? "6.0.0" : "5.1.0"); + const workflow = z + .object({ + jobs: z.object({ + "release-please": z.object({ + steps: z.array( + z.object({ + name: z.string().optional(), + run: z.string().optional(), + }), + ), + }), + }), + }) + .parse( + parse( + await readFile( + new URL("../../../.github/workflows/release.yml", import.meta.url), + "utf8", + ), + ), + ); + const command = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => + step.name === + "Validate checkpoint version and ancestry before release creation", + )?.run, + ); + const marker = join(f.directory, "next-release-action"); + const execution = execFileAsync( + "bash", + [ + "-e", + "-o", + "pipefail", + "-c", + `${command}\ntouch "$NEXT_RELEASE_ACTION_MARKER"`, + ], + { + cwd: fileURLToPath(new URL("../../../", import.meta.url)), + env: { + ...process.env, + GIT_DIR: join(f.directory, ".git"), + GIT_WORK_TREE: f.directory, + RELEASE_PHASE: "publish", + RELEASE_BRANCH: "release/6.0.0", + SOURCE_SHA: sourceSha, + NEXT_RELEASE_ACTION_MARKER: marker, + }, + }, + ); + if (valid) { + await expect(execution).resolves.toMatchObject({ stderr: "" }); + expect(await readFile(marker, "utf8")).toBe(""); + } else { + await expect(execution).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("release branch requires 6.0.0"), + }); + await expect(readFile(marker)).rejects.toMatchObject({ code: "ENOENT" }); + } + }, +); diff --git a/tests/boundary/filesystem/releaseWorkflow.test.ts b/tests/boundary/filesystem/releaseWorkflow.test.ts index e7a0c0935c..c8d404ef18 100644 --- a/tests/boundary/filesystem/releaseWorkflow.test.ts +++ b/tests/boundary/filesystem/releaseWorkflow.test.ts @@ -1,8 +1,464 @@ -import { readFile } from "node:fs/promises"; +import { execFile } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { delimiter, join } from "node:path"; +import { promisify } from "node:util"; import { expect, it } from "vitest"; import { parse } from "yaml"; import { z } from "zod"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execFileAsync = promisify(execFile); +const stepSchema = z.object({ + name: z.string().optional(), + uses: z.string().optional(), + run: z.string().optional(), + if: z.string().optional(), + with: z.record(z.string(), z.unknown()).optional(), + env: z.record(z.string(), z.unknown()).optional(), +}); +const jobSchema = z.object({ + if: z.string().optional(), + outputs: z.record(z.string(), z.string()).optional(), + steps: z.array(stepSchema), +}); + +async function readReleaseWorkflow() { + return z + .object({ + on: z.record(z.string(), z.unknown()), + jobs: z.object({ + "release-please": jobSchema, + publish: jobSchema, + "publish-mcp": jobSchema, + }), + }) + .parse( + parse( + await readFile( + new URL("../../../.github/workflows/release.yml", import.meta.url), + "utf8", + ), + ), + ); +} + +it("validates the source before release creation and the prepared candidate before generation", async () => { + const steps = (await readReleaseWorkflow()).jobs["release-please"].steps; + const source = steps.findIndex( + (step) => + step.name === + "Validate checkpoint version and ancestry before release creation", + ); + const action = steps.findIndex((step) => + step.uses?.startsWith("googleapis/release-please-action@"), + ); + const candidate = steps.findIndex( + (step) => + step.name === "Validate prepared release version and migration notes", + ); + const generation = steps.findIndex( + (step) => step.name === "Regenerate release documentation", + ); + expect(source).toBeGreaterThanOrEqual(0); + expect(source).toBeLessThan(action); + expect(steps[source]?.if).toBeUndefined(); + expect(steps[source]?.run).toContain("--source-sha"); + expect(candidate).toBeGreaterThan(action); + expect(candidate).toBeLessThan(generation); + expect(steps[candidate]?.if).toBe( + "inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'", + ); + expect(steps[candidate]?.run).toContain("--stage candidate"); + for (const name of [ + "Check out release controller and Git history", + "Check out release pull request", + ]) { + expect( + steps.find((step) => step.name === name)?.with?.["fetch-depth"], + ).toBe(0); + } +}); + +it("requires explicit release preparation or publication instead of main pushes", async () => { + const workflow = await readReleaseWorkflow(); + expect(Object.keys(workflow.on)).toEqual(["workflow_dispatch"]); + expect(workflow.on.workflow_dispatch).toMatchObject({ + inputs: { + release_branch: { required: true, type: "string" }, + phase: { + required: true, + type: "choice", + default: "prepare", + options: ["prepare", "publish"], + }, + }, + }); + const release = workflow.jobs["release-please"].steps.find((step) => + step.uses?.startsWith("googleapis/release-please-action@"), + ); + expect(release?.with).toMatchObject({ + token: "${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}", + "target-branch": "${{ inputs.release_branch }}", + "skip-github-release": "${{ inputs.phase == 'prepare' }}", + "skip-github-pull-request": "${{ inputs.phase == 'publish' }}", + }); + const catalogValidation = workflow.jobs["release-please"].steps.find( + (step) => step.name === "Validate generated release documentation", + ); + expect(catalogValidation?.run).toBe( + "npm run docs:check && git diff --exit-code", + ); +}); + +it.skipIf(process.platform === "win32").each([ + { phase: "prepare", customToken: "" }, + { phase: "publish", customToken: "" }, + { phase: "prepare", customToken: "fixture-custom-token" }, + { phase: "publish", customToken: "fixture-custom-token" }, +])( + "accepts $phase with optional custom credentials: $customToken", + async ({ phase, customToken }) => { + const workflow = await readReleaseWorkflow(); + const steps = workflow.jobs["release-please"].steps; + const actionIndex = steps.findIndex((step) => + step.uses?.startsWith("googleapis/release-please-action@"), + ); + expect(actionIndex).toBeGreaterThanOrEqual(0); + const commands = steps + .slice(0, actionIndex) + .filter((step) => + [ + "Validate release selection", + "Resolve release branch tip", + "Require the publish dispatch to match the branch tip", + ].includes(step.name ?? ""), + ) + .filter( + (step) => + step.if === undefined || + (phase === "publish" && step.if === "inputs.phase == 'publish'"), + ) + .map((step) => z.string().parse(step.run)); + const directory = await createTestTempDirectory("rea-release-credential-"); + const output = join(directory, "github-output"); + await writeFile( + join(directory, "gh"), + '#!/bin/sh\nprintf "%s\\n" "$GITHUB_SHA"\n', + { mode: 0o755 }, + ); + await expect( + execFileAsync( + "bash", + ["-e", "-o", "pipefail", "-c", commands.join("\n")], + { + env: { + ...process.env, + PATH: `${directory}${delimiter}${process.env.PATH ?? ""}`, + RELEASE_PLEASE_TOKEN: customToken, + GH_TOKEN: customToken || "fixture-built-in-token", + RELEASE_PHASE: phase, + RELEASE_BRANCH: "release/5.0.0", + GITHUB_REF: + phase === "prepare" + ? "refs/heads/main" + : "refs/heads/release/5.0.0", + GITHUB_REPOSITORY: "fixture/fixture", + GITHUB_SHA: "1111111111111111111111111111111111111111", + BRANCH_SHA: "1111111111111111111111111111111111111111", + GITHUB_OUTPUT: output, + }, + }, + ), + ).resolves.toMatchObject({ stderr: "" }); + expect(await readFile(output, "utf8")).toBe( + "sha=1111111111111111111111111111111111111111\n", + ); + }, +); + +it("binds npm and MCP publication to the same immutable release SHA", async () => { + const workflow = await readReleaseWorkflow(); + expect(workflow.jobs["release-please"].outputs).toEqual({ + release_created: "${{ steps.release.outputs.release_created }}", + sha: "${{ steps.release.outputs.sha }}", + }); + expect(workflow.jobs.publish.if).toBe( + "inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'", + ); + expect(workflow.jobs["publish-mcp"].if).toBe( + "inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'", + ); + for (const job of [workflow.jobs.publish, workflow.jobs["publish-mcp"]]) { + const checkout = job.steps.find((step) => + step.uses?.startsWith("actions/checkout@"), + ); + expect(checkout?.with).toMatchObject({ + ref: "${{ needs.release-please.outputs.sha }}", + "persist-credentials": false, + }); + } + const preparation = [ + "Check out release pull request", + "Set up Node.js for generated documentation", + "Install dependencies", + "Regenerate release documentation", + "Validate generated release documentation", + ]; + for (const name of preparation) { + expect( + workflow.jobs["release-please"].steps.find((step) => step.name === name) + ?.if, + ).toBe( + "inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'", + ); + } +}); + +it + .skipIf(process.platform === "win32") + .each(["main", "feature/new-work", "release/", "release/../main"])( + "rejects %s as a checkpoint before contacting GitHub", + async (releaseBranch) => { + const workflow = await readReleaseWorkflow(); + const command = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => step.name === "Validate release selection", + )?.run, + ); + await expect( + execFileAsync("bash", ["-e", "-o", "pipefail", "-c", command], { + env: { + ...process.env, + GITHUB_REF: "refs/heads/main", + RELEASE_BRANCH: releaseBranch, + RELEASE_PHASE: "prepare", + }, + }), + ).rejects.toMatchObject({ + code: 1, + stderr: expect.stringMatching(/release_branch/u), + }); + }, +); + +it.skipIf(process.platform === "win32").each([ + { phase: "prepare", ref: "refs/heads/feature/unreviewed-workflow" }, + { phase: "publish", ref: "refs/heads/main" }, + { phase: "publish", ref: "refs/heads/release/another-version" }, +])( + "rejects $phase from $ref before contacting GitHub", + async ({ phase, ref }) => { + const workflow = await readReleaseWorkflow(); + const command = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => step.name === "Validate release selection", + )?.run, + ); + await expect( + execFileAsync("bash", ["-e", "-o", "pipefail", "-c", command], { + env: { + ...process.env, + GITHUB_REF: ref, + RELEASE_BRANCH: "release/5.0.0", + RELEASE_PHASE: phase, + }, + }), + ).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining(`Run ${phase} from`), + }); + }, +); + +it.skipIf(process.platform === "win32")( + "checks the checkpoint locally and resolves the branch tip separately", + async () => { + const workflow = await readReleaseWorkflow(); + const validate = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => step.name === "Validate release selection", + )?.run, + ); + expect(validate).not.toMatch(/\bgh\b/u); + const resolve = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => step.name === "Resolve release branch tip", + )?.run, + ); + expect(resolve).toContain("gh api"); + for (const releaseBranch of ["release/5.0.0", "release/5.0.0-rc.1"]) { + await expect( + execFileAsync("bash", ["-e", "-o", "pipefail", "-c", validate], { + env: { + ...process.env, + GITHUB_REF: "refs/heads/main", + RELEASE_BRANCH: releaseBranch, + RELEASE_PHASE: "prepare", + }, + }), + ).resolves.toMatchObject({ stderr: "" }); + } + }, +); + +it + .skipIf(process.platform === "win32") + .each(["", "release", "prepare ", "true"])( + "rejects phase %j before contacting GitHub", + async (phase) => { + const workflow = await readReleaseWorkflow(); + const command = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => step.name === "Validate release selection", + )?.run, + ); + await expect( + execFileAsync("bash", ["-e", "-o", "pipefail", "-c", command], { + env: { + ...process.env, + GITHUB_REF: "refs/heads/main", + RELEASE_BRANCH: "release/5.0.0", + RELEASE_PHASE: phase, + }, + }), + ).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("phase must be prepare or publish"), + }); + }, +); + +it.skipIf(process.platform === "win32").each([true, false])( + "creates a release only when the branch tip is the dispatched checkpoint (match: %s)", + async (match) => { + const workflow = await readReleaseWorkflow(); + const command = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => + step.name === + "Require the publish dispatch to match the branch tip", + )?.run, + ); + const result = execFileAsync( + "bash", + ["-e", "-o", "pipefail", "-c", command], + { + env: { + ...process.env, + GITHUB_SHA: "1111111111111111111111111111111111111111", + BRANCH_SHA: match + ? "1111111111111111111111111111111111111111" + : "2222222222222222222222222222222222222222", + }, + }, + ); + if (match) { + await expect(result).resolves.toMatchObject({ stderr: "" }); + } else { + await expect(result).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "refusing to create a release for a moved source", + ), + }); + } + }, +); + +it.skipIf(process.platform === "win32").each([true, false])( + "publishes only when the tag and provenance source match (match: %s)", + async (match) => { + const workflow = await readReleaseWorkflow(); + const command = z + .string() + .parse( + workflow.jobs["release-please"].steps.find( + (step) => + step.name === "Bind publication to the dispatched checkpoint", + )?.run, + ); + const result = execFileAsync( + "bash", + ["-e", "-o", "pipefail", "-c", command], + { + env: { + ...process.env, + GITHUB_SHA: "1111111111111111111111111111111111111111", + RELEASE_SHA: match + ? "1111111111111111111111111111111111111111" + : "2222222222222222222222222222222222222222", + }, + }, + ); + if (match) { + await expect(result).resolves.toMatchObject({ stderr: "" }); + } else { + await expect(result).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "refusing to publish mismatched source provenance", + ), + }); + } + }, +); + +it("runs release PR CI without enabling implementation pushes on release branches", async () => { + const workflow = z + .object({ + on: z.object({ + push: z.object({ branches: z.array(z.string()) }), + pull_request: z.object({ branches: z.array(z.string()) }), + }), + }) + .parse( + parse( + await readFile( + new URL("../../../.github/workflows/ci.yml", import.meta.url), + "utf8", + ), + ), + ); + expect(workflow.on.push.branches).toEqual(["main"]); + expect(workflow.on.pull_request.branches).toEqual( + expect.arrayContaining(["main", "release/*"]), + ); +}); + +it("checks website changes on release-branch pull requests", async () => { + const workflow = z + .object({ + on: z.object({ + pull_request: z.object({ branches: z.array(z.string()) }), + }), + }) + .parse( + parse( + await readFile( + new URL( + "../../../.github/workflows/website-check.yml", + import.meta.url, + ), + "utf8", + ), + ), + ); + expect(workflow.on.pull_request.branches).toEqual(["main", "release/*"]); +}); + // Publishing is irreversible. Keep the release authority invariant as a static // boundary check; it is not proof that package installation or publishing works. it("requires package verification before publishing and retains the registry canary", async () => { diff --git a/tests/boundary/filesystem/runtimeIdentification.test.ts b/tests/boundary/filesystem/runtimeIdentification.test.ts deleted file mode 100644 index 7ebc2e6b90..0000000000 --- a/tests/boundary/filesystem/runtimeIdentification.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { - TextReader, - Uint8ArrayReader, - Uint8ArrayWriter, - ZipWriter, -} from "@zip.js/zip.js"; -import { describe, expect, it } from "vitest"; - -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js"; -import { parseEvidence } from "../../../src/domain/evidence.js"; -import { - identifyRuntimes, - runtimeIdentificationResultSchema, -} from "../../../src/domain/runtimeIdentification.js"; - -describe("runtime identification", () => { - it("identifies APK runtime families and exposes missing semantic providers", async () => { - const root = await createTestTempDirectory("rea-runtime-"); - const path = join(root, "Fixture.apk"); - await writeApkRuntimeFixture(path); - - const inventory = parseEvidence( - await runProviderAnalysis(path, "inventory_artifact", {}), - ); - const first = identifyRuntimes({ inventory_evidence: [inventory] }); - const second = identifyRuntimes({ inventory_evidence: [inventory] }); - expect(first).toEqual(second); - expect(runtimeIdentificationResultSchema.parse(first)).toMatchObject({ - root_format: "apk", - coverage: { - status: "complete-within-inventory", - inventory_complete: true, - }, - }); - expect(first.runtimes.map(({ family }) => family)).toEqual([ - "android", - "javascript", - "jvm", - "native", - "webassembly", - ]); - expect(first.runtimes).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - family: "android", - inspection: "available", - provider_id: "rea-android-application", - }), - expect.objectContaining({ - family: "jvm", - inspection: "provider-missing", - provider_id: null, - }), - expect.objectContaining({ - family: "webassembly", - inspection: "provider-missing", - provider_id: null, - }), - expect.objectContaining({ - family: "javascript", - inspection: "available", - provider_id: "rea-javascript-application", - }), - expect.objectContaining({ - family: "native", - inspection: "provider-selection-required", - provider_id: null, - }), - ]), - ); - const widened = runtimeIdentificationResultSchema.parse({ - ...first, - root_format: "x".repeat(101), - source_evidence_ids: Array.from( - { length: 101 }, - () => inventory.evidence_id, - ), - limitations: Array.from({ length: 101 }, () => "x".repeat(4_097)), - runtimes: first.runtimes.map((runtime) => ({ - ...runtime, - ...(runtime.provider_id === null - ? { reason: "x".repeat(1_001) } - : { provider_id: `provider-${"x".repeat(200)}` }), - observations: runtime.observations.map((observation) => ({ - ...observation, - path: "x".repeat(4_097), - format: "x".repeat(101), - })), - })), - }); - expect(widened.limitations).toHaveLength(101); - expect(widened.runtimes[0]?.observations[0]?.path).toHaveLength(4_097); - const available = first.runtimes.find( - ({ inspection }) => inspection === "available", - ); - if (available === undefined) - throw new TypeError("Expected an available runtime provider"); - expect( - runtimeIdentificationResultSchema.safeParse({ - ...first, - runtimes: [{ ...available, provider_id: null, reason: "Missing." }], - }).success, - ).toBe(false); - }); - - it("accepts more than 100 supplied inventory Evidence records", async () => { - const root = await createTestTempDirectory("rea-runtime-pages-"); - const path = join(root, "Fixture.apk"); - const writer = new ZipWriter(new Uint8ArrayWriter()); - await writer.add("one.js", new TextReader("one")); - await writeFile(path, await writer.close()); - const inventory = parseEvidence( - await runProviderAnalysis(path, "inventory_artifact", {}), - ); - const result = identifyRuntimes({ - inventory_evidence: Array.from({ length: 101 }, () => inventory), - }); - - expect(result.source_evidence_ids).toHaveLength(101); - expect(result.coverage.status).toBe("complete-within-inventory"); - expect(result.runtimes).toContainEqual( - expect.objectContaining({ - family: "javascript", - observations: expect.arrayContaining([ - expect.objectContaining({ path: "one.js" }), - ]), - }), - ); - }); - - it("does not route nested DEX content to the APK-only provider", async () => { - const root = await createTestTempDirectory("rea-runtime-dex-"); - const path = join(root, "Fixture.zip"); - const writer = new ZipWriter(new Uint8ArrayWriter()); - await writer.add( - "classes.dex", - new Uint8ArrayReader( - Uint8Array.from([...Buffer.from("dex\n035\0"), 0, 0, 0, 0]), - ), - ); - await writeFile(path, await writer.close()); - - const inventory = parseEvidence( - await runProviderAnalysis(path, "inventory_artifact", {}), - ); - const result = identifyRuntimes({ inventory_evidence: [inventory] }); - - expect(result.runtimes).toContainEqual( - expect.objectContaining({ - family: "android", - inspection: "provider-selection-required", - provider_id: null, - }), - ); - }); -}); - -const writeApkRuntimeFixture = async (path: string): Promise => { - const writer = new ZipWriter(new Uint8ArrayWriter()); - await writer.add( - "AndroidManifest.xml", - new Uint8ArrayReader(Uint8Array.from([3, 0, 8, 0, 8, 0, 0, 0])), - ); - await writer.add( - "classes.dex", - new Uint8ArrayReader( - Uint8Array.from([...Buffer.from("dex\n035\0"), 0, 0, 0, 0]), - ), - ); - await writer.add( - "assets/Fixture.class", - new Uint8ArrayReader( - Uint8Array.from([0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 61]), - ), - ); - await writer.add( - "assets/module.wasm", - new Uint8ArrayReader(Uint8Array.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0])), - ); - await writer.add("assets/main.js", new TextReader("bridge.call();")); - await writer.add( - "lib/arm64-v8a/libfixture.so", - new Uint8ArrayReader(Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0])), - ); - await writeFile(path, await writer.close()); -}; diff --git a/tests/boundary/filesystem/setupConfig.test.ts b/tests/boundary/filesystem/setupConfig.test.ts index 2996f630c1..e5e3ce25a2 100644 --- a/tests/boundary/filesystem/setupConfig.test.ts +++ b/tests/boundary/filesystem/setupConfig.test.ts @@ -243,36 +243,34 @@ describe("JSON client configuration migration and provider paths", () => { expect(await readFile(configPath, "utf8")).toBe("not-json"); }); - it.each(["null", "[]", '"value"'])( - "refuses a non-object JSON root without overwriting %s", - async (original) => { - const directory = await createTestTempDirectory("rea-setup-"); - const configPath = join(directory, "mcp.json"); - await writeFile(configPath, original); - expect(await configureJsonClient({ name: "cursor", configPath })).toEqual( - { status: "failed", reason: "readback" }, - ); - expect(await readFile(configPath, "utf8")).toBe(original); - await expect( - readFile(`${configPath}.rea.backup`, "utf8"), - ).rejects.toThrow(); - }, - ); + it("refuses a non-object JSON root without overwriting it", async () => { + const original = "null"; + const directory = await createTestTempDirectory("rea-setup-"); + const configPath = join(directory, "mcp.json"); + await writeFile(configPath, original); + expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({ + status: "failed", + reason: "readback", + }); + expect(await readFile(configPath, "utf8")).toBe(original); + await expect( + readFile(`${configPath}.rea.backup`, "utf8"), + ).rejects.toThrow(); + }); - it.each(["null", "[]", '"value"'])( - "preserves a non-object mcpServers value %s", - async (servers) => { - const directory = await createTestTempDirectory("rea-setup-"); - const configPath = join(directory, "mcp.json"); - const original = `{"mcpServers":${servers}}`; - await writeFile(configPath, original); - expect(await configureJsonClient({ name: "cursor", configPath })).toEqual( - { status: "failed", reason: "readback" }, - ); - expect(await readFile(configPath, "utf8")).toBe(original); - await expect( - readFile(`${configPath}.rea.backup`, "utf8"), - ).rejects.toThrow(); - }, - ); + it("preserves a non-object mcpServers value", async () => { + const servers = "[]"; + const directory = await createTestTempDirectory("rea-setup-"); + const configPath = join(directory, "mcp.json"); + const original = `{"mcpServers":${servers}}`; + await writeFile(configPath, original); + expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({ + status: "failed", + reason: "readback", + }); + expect(await readFile(configPath, "utf8")).toBe(original); + await expect( + readFile(`${configPath}.rea.backup`, "utf8"), + ).rejects.toThrow(); + }); }); diff --git a/tests/boundary/filesystem/setupSkill.test.ts b/tests/boundary/filesystem/setupSkill.test.ts index a0f0b47383..e90adeb1ba 100644 --- a/tests/boundary/filesystem/setupSkill.test.ts +++ b/tests/boundary/filesystem/setupSkill.test.ts @@ -1,4 +1,5 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -7,8 +8,14 @@ import { canonicalSkillNeedsInstall, installCanonicalSkill, } from "../../../src/application/SetupSkill.js"; +import { + runDoctor, + systemDoctorHost, +} from "../../../src/application/Doctor.js"; +import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; +import { z } from "zod"; import { skillReferenceIssues } from "../../../scripts/lib/docs-facts.mjs"; describe("canonical skill transaction", () => { @@ -83,6 +90,7 @@ describe("canonical skill transaction", () => { expect(installedSkill).toContain( `tool_count: ${String(TOOL_CONTRACTS.length)}`, ); + expect(installedSkill).not.toContain("catalog_digest:"); expect(await readFile(sibling, "utf8")).toBe("unrelated skill\n"); expect( await readFile( @@ -97,4 +105,102 @@ describe("canonical skill transaction", () => { expect(await installCanonicalSkill(home)).toBe("unchanged"); expect(await skillReferenceIssues(dirname(destination))).toEqual([]); }); + + it("binds portable conformance to the generated skill bytes that setup installs", async () => { + const home = await createTestTempDirectory("rea-skill-commitment-"); + expect(await installCanonicalSkill(home)).toBe("installed"); + const paths = [ + "SKILL.md", + "references/android-applications.md", + "references/evidence-workflows.md", + "references/javascript-applications.md", + "references/native-and-artifacts.md", + "references/runtime-observation.md", + ]; + const records = await Promise.all( + paths.map(async (path) => { + const bytes = await readFile( + join(home, ".agents/skills/reverse-engineer-anything", path), + ); + return `${path}\0${createHash("sha256").update(bytes).digest("hex")}\n`; + }), + ); + const manifest = z + .object({ + skill_digests: z.array( + z.object({ skill_id: z.string(), sha256: z.string() }), + ), + }) + .parse( + JSON.parse( + await readFile( + "docs/verification/managed-conformance-manifest.json", + "utf8", + ), + ), + ); + expect(manifest.skill_digests).toContainEqual({ + skill_id: "reverse-engineer-anything", + sha256: createHash("sha256").update(records.join("")).digest("hex"), + }); + }); +}); + +it("doctor verifies installed instructions and references even when metadata is current", async () => { + const home = await createTestTempDirectory("rea-skill-doctor-"); + const destination = join( + home, + ".agents/skills/reverse-engineer-anything/SKILL.md", + ); + const reference = join( + dirname(destination), + "references/evidence-workflows.md", + ); + const system = systemDoctorHost({ + environment: { HOME: home, USERPROFILE: home }, + }); + const host = createDoctorHostFixture({ + installedSkillIdentity: () => + system.installedSkillIdentity?.() ?? Promise.resolve(undefined), + }); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "missing", + ); + expect(await installCanonicalSkill(home)).toBe("installed"); + expect((await runDoctor(undefined, host)).identity?.skill).toMatchObject({ + state: "aligned", + installed_catalog_digest: null, + }); + const legacyDigest = "0".repeat(64); + await writeFile( + destination, + (await readFile(destination, "utf8")).replace( + ` tool_count: ${String(TOOL_CONTRACTS.length)}`, + ` tool_count: ${String(TOOL_CONTRACTS.length)}\n catalog_digest: "${legacyDigest}"`, + ), + ); + expect((await runDoctor(undefined, host)).identity?.skill).toMatchObject({ + state: "stale", + installed_catalog_digest: legacyDigest, + }); + expect(await installCanonicalSkill(home)).toBe("installed"); + for (const path of [destination, reference]) { + const canonical = await readFile(path, "utf8"); + await writeFile(path, `${canonical}\nLocally changed instructions.\n`); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "stale", + ); + expect(await installCanonicalSkill(home)).toBe("installed"); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "aligned", + ); + } + await rm(reference); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "stale", + ); + expect(await installCanonicalSkill(home)).toBe("installed"); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "aligned", + ); }); diff --git a/tests/boundary/filesystem/storeFileLock.test.ts b/tests/boundary/filesystem/storeFileLock.test.ts deleted file mode 100644 index e68f414dce..0000000000 --- a/tests/boundary/filesystem/storeFileLock.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import { lstat, readdir, rm } from "node:fs/promises"; -import { join } from "node:path"; - -import { describe, expect, it } from "vitest"; - -import { - createStoreFileLock, - removeStaleStoreFileLock, -} from "../../../src/application/StoreFileLock.js"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -describe("store lock cleanup ownership", () => { - it.each(["released", "replaced"] as const)( - "preserves a successor lock when an old %s owner cleans up", - async (state) => { - const root = await createTestTempDirectory("rea-store-lock-owner-"); - const path = join(root, "transaction.lock"); - const previous = await createStoreFileLock(path); - if (state === "released") await previous.release(); - else await rm(path, { recursive: true }); - const successor = await createStoreFileLock(path); - try { - await previous.release(); - await expect(lstat(path)).resolves.toBeDefined(); - await expect(createStoreFileLock(path)).rejects.toMatchObject({ - code: "EEXIST", - }); - } finally { - await successor.release(); - } - }, - ); -}); - -describe("store lock acquisition and recovery", () => { - it("admits one concurrent owner and removes all preparation directories", async () => { - const root = await createTestTempDirectory("rea-store-lock-contenders-"); - const path = join(root, "transaction.lock"); - const results = await Promise.allSettled( - Array.from({ length: 8 }, () => createStoreFileLock(path)), - ); - try { - expect( - results.filter((result) => result.status === "fulfilled"), - ).toHaveLength(1); - for (const result of results) - if (result.status === "rejected") - expect(result.reason).toMatchObject({ code: "EEXIST" }); - expect(await readdir(root)).toEqual(["transaction.lock"]); - expect(await removeStaleStoreFileLock(path)).toBe(false); - } finally { - await Promise.all( - results.map((result) => - result.status === "fulfilled" ? result.value.release() : undefined, - ), - ); - } - expect(await readdir(root)).toEqual([]); - }); - - it("recovers a directory lock abandoned by an exited process", async () => { - const root = await createTestTempDirectory("rea-store-lock-exited-"); - const path = join(root, "transaction.lock"); - await promisify(execFile)( - process.execPath, - ["tests/fixtures/abandonStoreFileLock.mjs", path], - { timeout: 5_000 }, - ); - expect((await lstat(path)).isDirectory()).toBe(true); - expect(await removeStaleStoreFileLock(path)).toBe(true); - const successor = await createStoreFileLock(path); - try { - expect(await removeStaleStoreFileLock(path)).toBe(false); - } finally { - await successor.release(); - } - expect(await readdir(root)).toEqual([]); - }); -}); diff --git a/tests/boundary/filesystem/temporaryDirectory.test.ts b/tests/boundary/filesystem/temporaryDirectory.test.ts deleted file mode 100644 index 14fa55ab24..0000000000 --- a/tests/boundary/filesystem/temporaryDirectory.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { access, realpath, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; - -import { describe, expect, it } from "vitest"; - -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -describe("test temporary directory ownership", { concurrent: false }, () => { - let completedDirectory: string | undefined; - - it("creates a canonical directory owned by the current test", async () => { - const directory = await createTestTempDirectory("rea-owned-temp-"); - completedDirectory = directory; - - expect(directory).toBe(await realpath(directory)); - expect(dirname(directory)).toBe(await realpath(tmpdir())); - await writeFile(join(directory, "nested.txt"), "owned"); - }); - - it("removes the prior test directory after that test finishes", async () => { - const directory = completedDirectory; - if (directory === undefined) - throw new Error("Prior test did not create a directory"); - await expect(access(directory)).rejects.toMatchObject({ - code: "ENOENT", - }); - }); - - it.each(["", "../escape-", "/absolute-", "missing-dash"])( - "rejects unsafe prefix %j", - async (prefix) => { - await expect(createTestTempDirectory(prefix)).rejects.toThrow(TypeError); - }, - ); -}); diff --git a/tests/boundary/filesystem/webScriptManifestPaths.test.ts b/tests/boundary/filesystem/webScriptManifestPaths.test.ts new file mode 100644 index 0000000000..9996fa8a99 --- /dev/null +++ b/tests/boundary/filesystem/webScriptManifestPaths.test.ts @@ -0,0 +1,77 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { isAbsolute, join, relative, sep } from "node:path"; +import { expect, it } from "vitest"; +import { LocalWebScriptArtifacts } from "../../../src/browser/assets/LocalWebScriptArtifacts.js"; +import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { webModuleArtifactsFixture } from "../../fixtures/webModuleTrace.js"; + +const materialize = async () => { + const root = join( + await createTestTempDirectory("rea-script-manifest-path-"), + "export with spaces", + ); + const artifact = webModuleArtifactsFixture(); + await mkdir(join(root, "files", "modules"), { recursive: true }); + await mkdir(join(root, "nested")); + const sourcePath = join(root, "files", "modules", "main.js"); + const manifestPath = join(root, "manifest.json"); + await writeFile(sourcePath, artifact.source); + await writeFile(manifestPath, JSON.stringify(artifact.manifest)); + return { root, artifact, sourcePath, manifestPath }; +}; + +it("reads equivalent local manifest spellings without changing reported identity", async () => { + const fixture = await materialize(); + const paths = new Set([ + fixture.manifestPath, + fixture.manifestPath.replaceAll("\\", "/"), + `${fixture.root}${sep}nested${sep}..${sep}manifest.json`, + ]); + const fromWorkingDirectory = relative(process.cwd(), fixture.manifestPath); + // A target on another Windows drive has no relative path from this cwd. + if (!isAbsolute(fromWorkingDirectory)) + paths.add(`.${sep}${fromWorkingDirectory}`); + for (const operation of [ + "trace_web_module_imports", + "trace_web_source_location", + ] as const) { + for (const manifestPath of paths) { + const response = await new LocalWebScriptArtifacts(operation).load({ + manifest_path: manifestPath, + script_index: 0, + }); + if (!response.ok) throw response.error; + expect(response.value.source).toBe(fixture.artifact.source); + expect(response.value.sourceFile).toEqual({ + ...fixture.artifact.sourceFile, + path: fixture.sourcePath, + }); + expect(response.value.manifestFile.path).toBe(manifestPath); + expect(response.value.manifest.output_directory).toBe( + fixture.artifact.manifest.output_directory, + ); + expect(response.value.manifest.scripts[0]?.url).toBe( + fixture.artifact.manifest.scripts[0]?.url, + ); + } + } +}); + +it("still rejects changed source bytes after normalizing the selected manifest path", async () => { + const fixture = await materialize(); + await writeFile(fixture.sourcePath, "tampered source"); + const response = await new LocalWebScriptArtifacts( + "trace_web_module_imports", + ).load({ + manifest_path: `${fixture.root}/nested/../manifest.json`.replaceAll( + "\\", + "/", + ), + script_index: 0, + }); + if (response.ok) throw new Error("Expected source integrity mismatch"); + expect(projectAnalysisError(response.error).code).toBe( + "artifact_integrity_mismatch", + ); +}); diff --git a/tests/boundary/filesystem/windowsNpxRegistration.test.ts b/tests/boundary/filesystem/windowsNpxRegistration.test.ts new file mode 100644 index 0000000000..dc50f232ec --- /dev/null +++ b/tests/boundary/filesystem/windowsNpxRegistration.test.ts @@ -0,0 +1,168 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Client } from "@modelcontextprotocol/client"; +import { + StdioClientTransport, + getDefaultEnvironment, +} from "@modelcontextprotocol/client/stdio"; +import { expect, it } from "vitest"; +import { z } from "zod"; +import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js"; +import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js"; +import { setupRegistrationCommand } from "../../../src/application/SetupHost.js"; +import { systemUninstallHost } from "../../../src/application/Uninstall.js"; +import { PRODUCT_IDENTITY } from "../../../src/identity.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const configurationSchema = z.object({ + mcpServers: z.object({ + rea: z.object({ command: z.string(), args: z.array(z.string()) }), + }), +}); + +it("migrates bare Windows npx registrations idempotently and uninstalls only REA", async () => { + const home = await createTestTempDirectory("rea-windows-registration-"); + const configPath = join(home, ".claude.json"); + const original = JSON.stringify({ + theme: "dark", + mcpServers: { + other: { command: "another-server" }, + rea: { + command: "npx", + args: ["-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"], + }, + }, + }); + await writeFile(configPath, original); + expect( + await readClientRegistrationStatuses(home, undefined, { + platform: "win32", + environment: {}, + }), + ).toContainEqual( + expect.objectContaining({ client: "claude_code", state: "stale" }), + ); + const client = { name: "claude_code", configPath, format: "json" } as const; + const command = setupRegistrationCommand("win32", true); + expect(await configureClientConfiguration(client, {}, command)).toMatchObject( + { status: "configured" }, + ); + expect( + await readClientRegistrationStatuses(home, undefined, { + platform: "win32", + environment: {}, + }), + ).toContainEqual( + expect.objectContaining({ client: "claude_code", state: "aligned" }), + ); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "removed", + }); + const remaining: unknown = JSON.parse(await readFile(configPath, "utf8")); + expect(remaining).toEqual({ + theme: "dark", + mcpServers: { other: { command: "another-server" } }, + }); +}); + +it("keeps the Windows launcher aligned through Codex TOML setup and removal", async () => { + const home = await createTestTempDirectory("rea-windows-codex-"); + await mkdir(join(home, ".codex")); + const configPath = join(home, ".codex", "config.toml"); + const original = '[mcp_servers.other]\ncommand = "another-server"\n'; + await writeFile(configPath, original); + const client = { name: "codex", configPath, format: "toml" } as const; + const command = setupRegistrationCommand("win32", true); + expect(await configureClientConfiguration(client, {}, command)).toMatchObject( + { status: "configured" }, + ); + expect( + await readClientRegistrationStatuses(home, undefined, { + platform: "win32", + environment: {}, + }), + ).toContainEqual( + expect.objectContaining({ client: "codex", state: "aligned" }), + ); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "removed", + }); + const remaining = await readFile(configPath, "utf8"); + expect(remaining).toContain("[mcp_servers.other]"); + expect(remaining).toContain('command = "another-server"'); + expect(remaining).not.toContain("mcp_servers.rea"); +}); + +it.skipIf(process.platform !== "win32")( + "starts the written Windows registration through a native npx.cmd shim", + async () => { + const root = await createTestTempDirectory("rea-windows-mcp-"); + const home = join(root, "home with spaces"); + await mkdir(home); + const configPath = join(home, ".claude.json"); + const setupClient = { + name: "claude_code", + configPath, + format: "json", + } as const; + // The default writer and npm-run setup must produce the same native launcher. + expect(await configureClientConfiguration(setupClient)).toMatchObject({ + status: "configured", + }); + const decoded: unknown = JSON.parse(await readFile(configPath, "utf8")); + const registration = configurationSchema.parse(decoded).mcpServers.rea; + expect([registration.command, ...registration.args]).toEqual( + setupRegistrationCommand("win32", true), + ); + const entry = fileURLToPath( + new URL("../../../scripts/rea.mjs", import.meta.url), + ); + // Exercise the actual batch-shim boundary without registry access or package installation. + await writeFile( + join(home, "npx.cmd"), + [ + "@echo off", + 'if not "%~1"=="-y" exit /b 11', + `if not "%~2"=="${PRODUCT_IDENTITY.registrationPackageSpecifier}" exit /b 12`, + 'if not "%~3"=="mcp" exit /b 13', + `"${process.execPath}" "${entry}" mcp`, + "", + ].join("\r\n"), + ); + const transport = new StdioClientTransport({ + command: registration.command, + args: registration.args, + cwd: home, + env: getDefaultEnvironment(), + stderr: "pipe", + }); + const client = new Client({ name: "windows-setup-launcher", version: "1" }); + try { + await client.connect(transport); + expect(client.getServerVersion()?.version).toBe( + PRODUCT_IDENTITY.packageVersion, + ); + const status = await client.callTool({ + name: "binary_session", + arguments: {}, + }); + expect(status.isError).not.toBe(true); + expect(status.structuredContent).toMatchObject({ + result: { open: false }, + }); + } finally { + await client.close(); + await transport.close(); + } + }, + 30_000, +); diff --git a/tests/boundary/filesystem/zipDetection.test.ts b/tests/boundary/filesystem/zipDetection.test.ts index 236589f143..8ab81112ac 100644 --- a/tests/boundary/filesystem/zipDetection.test.ts +++ b/tests/boundary/filesystem/zipDetection.test.ts @@ -3,27 +3,22 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { classifyRoot } from "../../../src/application/ArtifactInventory/classify.js"; +import { classifyRoot } from "../../../src/artifacts/inventory/classify.js"; import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("ZIP signature detection", () => { it.each([ - [0x03, 0x04, true], - [0x05, 0x06, true], - [0x07, 0x08, true], - [0x03, 0x06, false], - [0x03, 0x08, false], - [0x05, 0x04, false], - [0x05, 0x08, false], - [0x07, 0x04, false], - [0x07, 0x06, false], + { signature: [0x50, 0x4b, 0x05, 0x06], archive: true }, + { signature: [0x50, 0x4b, 0x03, 0x06], archive: false }, + { signature: [0x50, 0x4b, 0x03, 0x04], archive: true }, + { signature: [0x50, 0x4b, 0x07, 0x08], archive: true }, ] as const)( - "classifies PK %i %i with archive signature %s", - async (third, fourth, archive) => { + "routes representative ZIP signature $signature through inventory and target parsing", + async ({ signature, archive }) => { const root = await createTestTempDirectory("rea-zip-signature-"); const path = join(root, "input"); - await writeFile(path, Buffer.from([0x50, 0x4b, third, fourth])); + await writeFile(path, Buffer.from(signature)); expect(await classifyRoot(path, false)).toBe(archive ? "zip" : "file"); const target = await parseBinaryTarget(path); if (archive) diff --git a/tests/boundary/javascript/packageConditionalExports.test.ts b/tests/boundary/javascript/packageConditionalExports.test.ts index 05b4465ef5..7c65b6d49b 100644 --- a/tests/boundary/javascript/packageConditionalExports.test.ts +++ b/tests/boundary/javascript/packageConditionalExports.test.ts @@ -237,18 +237,23 @@ describe("unmatched package export conditions", () => { }, ); it("does not report valid metadata as malformed", async () => { - const { outcome } = await compareWithNode({ default: 7 }, "import"); - expect(outcome.resolution_status).toBe("unavailable"); - expect(outcome.limitations[0]).toContain("not valid package JSON"); + const { node_error, outcome } = await compareWithNode( + { default: 7 }, + "import", + ); + expect(node_error).toBe("ERR_INVALID_PACKAGE_TARGET"); + expect(outcome.resolution_status).toBe("rejected"); + expect(outcome.limitations[0]).toContain("exports target 7"); + expect(outcome.limitations[0]).not.toContain("not valid package JSON"); }); }); describe("package exports fallback refusal reasons", () => { it.each([ - [{ import: [42, 7] }, "ERR_INVALID_PACKAGE_TARGET", "unavailable"], + [{ import: [42, 7] }, "ERR_INVALID_PACKAGE_TARGET", "rejected"], [{ import: [null, null] }, "ERR_PACKAGE_PATH_NOT_EXPORTED", "external"], [{ import: [42, null] }, "ERR_PACKAGE_PATH_NOT_EXPORTED", "external"], - [{ import: [null, 42] }, "ERR_INVALID_PACKAGE_TARGET", "unavailable"], + [{ import: [null, 42] }, "ERR_INVALID_PACKAGE_TARGET", "rejected"], [ { import: { node: null }, default: "./default.cjs" }, "ERR_PACKAGE_PATH_NOT_EXPORTED", @@ -265,6 +270,10 @@ describe("package exports fallback refusal reasons", () => { expect(node_error).toBe(code); expect(outcome.resolved_path).toBeNull(); expect(outcome.resolution_status).toBe(status); + if (code === "ERR_INVALID_PACKAGE_TARGET") { + expect(outcome.limitations[0]).toContain("exports target"); + expect(outcome.limitations[0]).not.toContain("not valid package JSON"); + } }, ); }); diff --git a/tests/boundary/mcp/androidAnalysisMcp.test.ts b/tests/boundary/mcp/androidAnalysisMcp.test.ts index 55e335335c..dcb8297ede 100644 --- a/tests/boundary/mcp/androidAnalysisMcp.test.ts +++ b/tests/boundary/mcp/androidAnalysisMcp.test.ts @@ -1,5 +1,7 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it as test, onTestFinished } from "vitest"; +import { z } from "zod"; import { access } from "node:fs/promises"; import { ANDROID_TOOL_CONTRACTS } from "../../../src/contracts/android/androidToolContracts.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; @@ -92,8 +94,15 @@ it("publishes and executes all APK contracts with inline Evidence and no active await server.connect(serverTransport); await client.connect(clientTransport); const advertised = await client.listTools(); + const wireTools = new Map(advertised.tools.map((tool) => [tool.name, tool])); for (const contract of ANDROID_TOOL_CONTRACTS) { - const wire = advertised.tools.find((tool) => tool.name === contract.name); + const wire = wireTools.get(contract.name); + if (wire === undefined || wire.outputSchema === undefined) + throw new Error(`Missing advertised output schema for ${contract.name}`); + const validateOutput = new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(z.record(z.string(), z.unknown()).parse(wire.outputSchema)); expect(wire?.annotations).toMatchObject(contract.annotations); expect(contract.effects).toMatchObject({ mutatesTarget: false, @@ -120,6 +129,7 @@ it("publishes and executes all APK contracts with inline Evidence and no active arguments: input, }); expect(response.isError, JSON.stringify(response)).not.toBe(true); + expect(validateOutput(response.structuredContent)).toBe(true); const result = contract.outputSchema.parse(response.structuredContent); const evidence = parseEvidence(result.evidence); expect(result.result).toEqual(evidence.normalized_result); diff --git a/tests/boundary/mcp/artifactEntryOrderMcp.test.ts b/tests/boundary/mcp/artifactEntryOrderMcp.test.ts deleted file mode 100644 index a3ad286716..0000000000 --- a/tests/boundary/mcp/artifactEntryOrderMcp.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { join } from "node:path"; - -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; -import { expect, it } from "vitest"; -import { z } from "zod"; - -import { parseConfig } from "../../../src/config.js"; -import { createBinarySession } from "../../../src/composition/binary.js"; -import { createServer } from "../../../src/server/createServer.js"; -import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; -import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; -import { - artifactParentPaths, - writeOrderedZip, -} from "../../fixtures/artifactEntryOrder.js"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -it("returns correct child-first ZIP containment through MCP inspection", async () => { - const root = await createTestTempDirectory("rea-entry-order-mcp-"); - const archive = join(root, "child-first.zip"); - await writeOrderedZip(archive, ["pkg/sub/data.txt", "pkg/sub/", "pkg/"]); - const configured = parseConfig({}); - if (!configured.ok) throw configured.error; - const session = createBinarySession(configured.value); - const server = createServer(session, session); - const client = new Client({ name: "entry-order-test", version: "1" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - try { - await server.connect(serverTransport); - await client.connect(clientTransport); - expect( - ( - await client.callTool({ - name: "open_binary", - arguments: { path: archive }, - }) - ).isError, - ).not.toBe(true); - const called = await client.callTool({ - name: "inspect_artifact", - arguments: {}, - }); - expect(called.isError, JSON.stringify(called.structuredContent)).not.toBe( - true, - ); - const inspection = artifactInspectionResultSchema.parse( - z.object({ result: z.unknown() }).parse(called.structuredContent).result, - ); - const substep = inspection.substeps[0]; - if (substep === undefined) throw new Error("Missing inventory Evidence"); - const inventory = artifactInventoryResultSchema.parse( - substep.evidence.normalized_result, - ); - expect(artifactParentPaths(inventory)).toEqual({ - ".": null, - pkg: ".", - "pkg/sub": "pkg", - "pkg/sub/data.txt": "pkg/sub", - }); - expect(inspection.coverage.status).toBe("complete-within-substeps"); - } finally { - await client.close(); - await server.close(); - await session.close(); - } -}); diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index a0bf858754..6d14f9df41 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -5,8 +5,6 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { expect, it } from "vitest"; import { z } from "zod"; -import { parseConfig } from "../../../src/config.js"; -import { createBinarySession } from "../../../src/composition/binary.js"; import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -64,6 +62,40 @@ it("inspects a standalone keyed archive through MCP with original object identit arguments: { path: "other.plist" }, }); expect(rejected.isError).toBe(true); + expect(rejected.structuredContent).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [ + { + path: ["path"], + reason: "invalid_value", + expected: "archive.plist", + }, + ], + }, + }, + }); + const missingRoot = await client.callTool({ + name: "inspect_keyed_archive", + arguments: { root: "missing" }, + }); + expect(missingRoot.isError).toBe(true); + expect(missingRoot.structuredContent).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [ + { + path: ["root"], + reason: "invalid_value", + message: expect.stringContaining("missing"), + expected: Object.keys(graph.roots), + }, + ], + }, + }, + }); } finally { await client.close(); await server.close(); @@ -71,25 +103,19 @@ it("inspects a standalone keyed archive through MCP with original object identit } }); -it("returns every artifact occurrence in one MCP call", async () => { +it("returns all 520 ZIP file occurrences in one inspect_artifact MCP result", async () => { const directory = await createTestTempDirectory("rea-artifact-inline-"); const archive = join(directory, "many.zip"); const writer = new ZipWriter(new Uint8ArrayWriter()); const fileCount = 520; - for (let index = 0; index < fileCount; index += 1) { + for (let index = 0; index < fileCount; index += 1) await writer.add( `files/${String(index)}.txt`, new TextReader(`file-${String(index)}`), ); - } await writeFile(archive, await writer.close()); - const configured = parseConfig({ - HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper", - }); - expect(configured.ok).toBe(true); - if (!configured.ok) throw configured.error; - const session = createBinarySession(configured.value); + const session = createTestBinarySession(new ArtifactProvider()); const server = createServer(session, session); const client = new Client({ name: "artifact-inline-test", version: "1" }); const [clientTransport, serverTransport] = @@ -102,43 +128,6 @@ it("returns every artifact occurrence in one MCP call", async () => { arguments: { path: archive }, }); expect(opened.isError).not.toBe(true); - const names = (await client.listTools()).tools.map(({ name }) => name); - expect(names).toContain("inspect_artifact"); - expect(names).toContain("inspect_asset_catalog"); - expect(names).not.toContain("inventory_artifact"); - const status = await client.callTool({ - name: "binary_session", - arguments: {}, - }); - expect(status.isError).not.toBe(true); - const capabilities = z - .object({ - result: z.object({ - capabilities: z.array( - z.object({ - operation: z.string(), - available: z.boolean(), - reason: z.string().nullable(), - }), - ), - }), - }) - .parse(status.structuredContent).result.capabilities; - expect(capabilities.map(({ operation }) => operation)).not.toContain( - "inventory_artifact", - ); - expect( - capabilities.find( - ({ operation }) => operation === "inspect_asset_catalog", - ), - ).toMatchObject( - process.platform === "darwin" - ? { available: true, reason: null } - : { - available: false, - reason: "Apple asset catalogs require macOS assetutil.", - }, - ); const result = await client.callTool({ name: "inspect_artifact", arguments: {}, @@ -157,23 +146,14 @@ it("returns every artifact occurrence in one MCP call", async () => { }), }), ), - observations: z.array(z.unknown()), - derived_relationships: z.array(z.unknown()), - hypotheses: z.array(z.unknown()), - unexplored_branches: z.array(z.unknown()), - next_probes: z.array(z.unknown()), }) .parse( z.object({ result: z.unknown() }).parse(result.structuredContent) .result, ); - const occurrences = - inspection.substeps[0]?.evidence.normalized_result.occurrences; - if (occurrences === undefined) - throw new Error("inspection omitted the artifact inventory"); - expect(occurrences).toHaveLength(fileCount + 1); - expect(inspection.observations.length).toBeGreaterThan(0); - expect(inspection.derived_relationships.length).toBeGreaterThan(0); + expect( + inspection.substeps[0]?.evidence.normalized_result.occurrences, + ).toHaveLength(fileCount + 1); } finally { await Promise.allSettled([client.close(), server.close(), session.close()]); } diff --git a/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts b/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts new file mode 100644 index 0000000000..12ed431d96 --- /dev/null +++ b/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts @@ -0,0 +1,76 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it, onTestFinished } from "vitest"; +import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + binaryLayoutFixture, + BINARY_LAYOUT_TEST_PROVIDER, +} from "../../fixtures/binaryDiagnostics/layout.js"; + +it("advertises exact valid schemas and records inline layout without a binary target", async () => { + const value = binaryLayoutFixture(); + const service = new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(value)), + }); + const session = createTestBinarySession(() => { + throw new Error("deep provider must not start"); + }); + const server = createServer(session, session, { binaryLayout: service }); + const client = new Client({ name: "binary-layout-contract", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = (await client.listTools()).tools.find( + (tool) => tool.name === "inspect_binary_layout", + ); + if (advertised?.outputSchema === undefined) + throw new Error("layout must publish both schemas"); + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + const inputSchema: Record = advertised.inputSchema; + const outputSchema: Record = advertised.outputSchema; + expect(ajv.validateSchema(inputSchema)).toBe(true); + expect(ajv.validateSchema(outputSchema)).toBe(true); + expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true); + expect( + ajv.validate(inputSchema, { + path: value.artifact.path, + approval: true, + }), + ).toBe(false); + const response = await client.callTool({ + name: "inspect_binary_layout", + arguments: { path: value.artifact.path }, + }); + expect(response.isError).not.toBe(true); + expect( + ajv.validate(outputSchema, response.structuredContent), + JSON.stringify(ajv.errors), + ).toBe(true); + const parsed = toolContract("inspect_binary_layout").outputSchema.parse( + response.structuredContent, + ); + const evidence = parseEvidence(parsed.evidence); + expect(parsed.result).toEqual(value); + expect(parsed.result).toEqual(evidence.normalized_result); + expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence); + expect( + ( + await client.callTool({ + name: "inspect_binary_layout", + arguments: { path: "relative.elf" }, + }) + ).isError, + ).toBe(true); +}); diff --git a/tests/boundary/mcp/browserMcp.test.ts b/tests/boundary/mcp/browserMcp.test.ts index 21e615e622..a61576ee77 100644 --- a/tests/boundary/mcp/browserMcp.test.ts +++ b/tests/boundary/mcp/browserMcp.test.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; +import { webPageInspectionSchema } from "../../../src/domain/browserObservation.js"; import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { createServer } from "../../../src/server/createServer.js"; @@ -190,6 +191,40 @@ const verifySessionAndComparisonTools = async ( expect(compared.structuredContent).toMatchObject({ result: { overall_status: "unknown" }, }); + const parsedCapture = webPageInspectionSchema.parse(capture); + const socket = parsedCapture.network.websocket_events.find( + (event) => event.payload_shape?.json_shape != null, + ); + const payload = socket?.payload_shape; + if (socket === undefined || payload == null || payload.json_shape === null) + throw new Error("Missing deep WebSocket JSON shape fixture"); + const malformedCapture = { + ...parsedCapture, + network: { + ...parsedCapture.network, + websocket_events: [ + { + ...socket, + payload_shape: { + ...payload, + json_shape: { + ...payload.json_shape, + properties: [{ path: "/event", types: [42], observations: 1 }], + }, + }, + }, + ], + }, + }; + const malformedComparison = await connected.client.callTool({ + name: "compare_web_captures", + arguments: { + before: { inspection: malformedCapture }, + after: { inspection: capture }, + }, + }); + expect(malformedComparison.isError).toBe(true); + expect(JSON.stringify(malformedComparison.content)).toContain("types"); const incompleteComparison = await connected.client.callTool({ name: "compare_web_captures", arguments: { before: { inspection: capture } }, diff --git a/tests/boundary/mcp/bundleComparisonMcp.test.ts b/tests/boundary/mcp/bundleComparisonMcp.test.ts index ebc86e4906..ab1343bd7c 100644 --- a/tests/boundary/mcp/bundleComparisonMcp.test.ts +++ b/tests/boundary/mcp/bundleComparisonMcp.test.ts @@ -106,6 +106,42 @@ describe("bundle comparison MCP integration", () => { await connected.close(); } }); + + it("explains that a single Evidence record is not a bundle", async () => { + const root = await createTestTempDirectory("rea-bundle-record-"); + roots.push(root); + const record = sourceEvidence("single"); + const recordPath = join(root, "record.json"); + const bundlePath = join(root, "bundle.json"); + await Promise.all([ + writeFile(recordPath, JSON.stringify(record)), + writeFile( + bundlePath, + serializeEvidenceBundle(createEvidenceBundle([record])), + ), + ]); + const connected = await connect(); + try { + const result = await connected.client.callTool({ + name: "compare_bundles", + arguments: { + left_bundle_path: recordPath, + right_bundle_path: bundlePath, + }, + }); + expect(result.isError).toBe(true); + expect(result.structuredContent).toMatchObject({ + error: { + code: "evidence_integrity_mismatch", + message: expect.stringContaining( + `this JSON is one Evidence record (${record.evidence_id})`, + ), + }, + }); + } finally { + await connected.close(); + } + }); }); const connect = async () => { diff --git a/tests/boundary/mcp/createServer.test.ts b/tests/boundary/mcp/createServer.test.ts deleted file mode 100644 index 781a4162a9..0000000000 --- a/tests/boundary/mcp/createServer.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect } from "vitest"; - -import { observed as ok } from "../../fixtures/analysisExecution.js"; -import { mcpTest } from "../../support/mcp/mcpFixture.js"; -import { createServer } from "../../../src/server/createServer.js"; - -const analysis = { - execute: () => Promise.resolve(ok(null)), -}; - -describe("MCP server composition", () => { - mcpTest("does not advertise unavailable session tools", async ({ mcp }) => { - const server = createServer(analysis); - const client = await mcp.connect(server, { - name: "composition-test", - version: "1.0.0", - }); - expect(client.getInstructions()).not.toContain("open_binary"); - const names = (await client.listTools()).tools.map(({ name }) => name); - expect(names).not.toContain("compare_managed_members"); - }); -}); diff --git a/tests/boundary/mcp/dylibResolutionMcp.test.ts b/tests/boundary/mcp/dylibResolutionMcp.test.ts new file mode 100644 index 0000000000..179912a182 --- /dev/null +++ b/tests/boundary/mcp/dylibResolutionMcp.test.ts @@ -0,0 +1,195 @@ +import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; +import { + FILE_TYPE, + LC, + dylibCommand, + machoImage, + rpathCommand, +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; +import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const withClient = async ( + verify: (client: Client) => Promise, +): Promise => { + const session = createTestBinarySession(new ArtifactProvider()); + const server = createServer(session, session); + const client = new Client({ name: "dylib-mcp-test", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await verify(client); + } finally { + await client.close(); + await server.close(); + } +}; + +const structuredResult = (value: unknown): unknown => + z.object({ result: z.unknown() }).parse(value).result; + +it("traces dylib resolution for an opened app bundle and rejects a changed target", async () => { + const app = join( + await createTestTempDirectory("rea-dylib-mcp-"), + "Fixture.app", + ); + await mkdir(join(app, "Contents/MacOS"), { recursive: true }); + await mkdir(join(app, "Contents/Frameworks"), { recursive: true }); + await writeFile( + join(app, "Contents/Info.plist"), + "CFBundleExecutableApp", + ); + const executable = join(app, "Contents/MacOS/App"); + await writeFile( + executable, + machoImage({ + commands: [ + rpathCommand("@executable_path/../Frameworks"), + dylibCommand(LC.LOAD_DYLIB, "@rpath/libcore.dylib"), + ], + }), + ); + await writeFile( + join(app, "Contents/Frameworks/libcore.dylib"), + machoImage({ fileType: FILE_TYPE.dylib }), + ); + await withClient(async (client) => { + const opened = await client.callTool({ + name: "open_binary", + arguments: { path: app }, + }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const called = await client.callTool({ + name: "trace_dylib_resolution", + arguments: {}, + }); + expect(called.isError, JSON.stringify(called.structuredContent)).not.toBe( + true, + ); + const trace = dylibResolutionResultSchema.parse( + structuredResult(called.structuredContent), + ); + expect(trace.edges).toEqual([ + expect.objectContaining({ + install_name: "@rpath/libcore.dylib", + resolution: { + status: "resolved", + image: "Contents/Frameworks/libcore.dylib", + }, + }), + ]); + const escaping = await client.callTool({ + name: "trace_dylib_resolution", + arguments: { roots: ["Contents/../../outside"] }, + }); + expect(escaping.isError).toBe(true); + + await writeFile(executable, machoImage({})); + const changed = await client.callTool({ + name: "trace_dylib_resolution", + arguments: {}, + }); + expect(changed.isError).toBe(true); + expect(JSON.stringify(changed.structuredContent)).toContain("integrity"); + }); +}); + +it("rejects targets that are not Mach-O", async () => { + const directory = await createTestTempDirectory("rea-dylib-mcp-plist-"); + const path = join(directory, "Info.plist"); + await writeFile(path, ""); + await withClient(async (client) => { + const opened = await client.callTool({ + name: "open_binary", + arguments: { path }, + }); + expect(opened.isError).not.toBe(true); + const called = await client.callTool({ + name: "trace_dylib_resolution", + arguments: {}, + }); + expect(called.isError).toBe(true); + expect(JSON.stringify(called.structuredContent)).toContain("unavailable"); + }); +}); + +it("traces a standalone Mach-O file whose name ends in .app", async () => { + const directory = await createTestTempDirectory("rea-dylib-mcp-file-"); + await writeFile( + join(directory, "Tool.app"), + machoImage({ + commands: [dylibCommand(LC.LOAD_DYLIB, "@loader_path/libcore.dylib")], + }), + ); + await writeFile( + join(directory, "libcore.dylib"), + machoImage({ fileType: FILE_TYPE.dylib }), + ); + await withClient(async (client) => { + const opened = await client.callTool({ + name: "open_binary", + arguments: { path: join(directory, "Tool.app") }, + }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const called = await client.callTool({ + name: "trace_dylib_resolution", + arguments: {}, + }); + expect(called.isError, JSON.stringify(called.structuredContent)).not.toBe( + true, + ); + const trace = dylibResolutionResultSchema.parse( + structuredResult(called.structuredContent), + ); + expect(trace.edges).toEqual([ + expect.objectContaining({ + resolution: { status: "resolved", image: "libcore.dylib" }, + }), + ]); + }); +}); + +it.skipIf(process.getuid?.() === 0)( + "keeps a permission denied while canonicalizing the opened bundle", + async () => { + const parent = await createTestTempDirectory("rea-dylib-mcp-revoked-"); + const app = join(parent, "Revoked.app"); + await mkdir(join(app, "Contents/MacOS"), { recursive: true }); + await writeFile( + join(app, "Contents/Info.plist"), + "CFBundleExecutableApp", + ); + await writeFile(join(app, "Contents/MacOS/App"), machoImage({})); + await withClient(async (client) => { + const opened = await client.callTool({ + name: "open_binary", + arguments: { path: app }, + }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + await chmod(parent, 0o000); + try { + const called = await client.callTool({ + name: "trace_dylib_resolution", + arguments: {}, + }); + expect(called.isError).toBe(true); + expect(JSON.stringify(called.structuredContent)).toMatch( + /Permission denied \(EACCES\)/u, + ); + } finally { + await chmod(parent, 0o755); + } + }); + }, +); diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index 72d8892ede..4cc589e702 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -10,6 +10,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import type { ElectronActiveObservationPort } from "../../../src/application/javascript/ElectronActiveObservationPort.js"; +import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js"; import { CdpElectronProvider } from "../../../src/browser/CdpElectronProvider.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed } from "../../fixtures/analysisExecution.js"; @@ -270,12 +271,18 @@ it("exposes the target-free static JavaScript application workflow", async () => await server.connect(serverTransport); await client.connect(clientTransport); - const analyzed = await client.callTool({ - name: "analyze_javascript_application", - arguments: { input_path: root }, - }); + const progress: number[] = []; + const analyzed = await client.callTool( + { + name: "analyze_javascript_application", + arguments: { input_path: root }, + }, + { onprogress: (update) => progress.push(update.progress) }, + ); expect(analyzed.isError).not.toBe(true); + expect(progress[0]).toBe(0); + expect(progress.at(-1)).toBe(1); const projected = z .object({ evidence_id: z.string(), @@ -314,6 +321,9 @@ it("exposes the target-free static JavaScript application workflow", async () => expect(projected.result.graph.nodes.length).toBeGreaterThan(0); expect(projected.result.semantic_graph.nodes.length).toBeGreaterThan(0); expect(projected.evidence.operation).toBe("analyze_javascript_application"); + const direct = await analyzeJavaScriptApplication({ input_path: root }); + if (!direct.ok) throw direct.error; + expect(projected.evidence_id).toBe(direct.value.evidence_id); }); const evidenceFor = (value: unknown) => { diff --git a/tests/boundary/mcp/enhancedTools.06-rejects-incomplete-function-dossiers-at.test.ts b/tests/boundary/mcp/enhancedTools.06-rejects-incomplete-function-dossiers-at.test.ts index 0b9fe43efe..d0ac870fc6 100644 --- a/tests/boundary/mcp/enhancedTools.06-rejects-incomplete-function-dossiers-at.test.ts +++ b/tests/boundary/mcp/enhancedTools.06-rejects-incomplete-function-dossiers-at.test.ts @@ -1,123 +1,18 @@ -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { afterEach, describe, expect, it } from "vitest"; -import { PROCEDURES, inventory } from "./enhancedToolsHarness.js"; -import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; -import { createServer } from "../../../src/server/createServer.js"; -import { observed as ok } from "../../fixtures/analysisExecution.js"; +import { functionDossierSchema } from "../../../src/domain/hopperValues.js"; +import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js"; +import { observed } from "../../fixtures/analysisExecution.js"; +import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js"; -const fixturePort = (): AnalysisOperationPort => ({ - execute: (name, arguments_) => { - switch (name) { - case "list_procedures": - return Promise.resolve(ok(inventory(PROCEDURES))); - case "list_names": - return Promise.resolve( - ok( - inventory({ - "0x10": "_OBJC_CLASS_$_Fixture", - "0x11": "_OBJC_CLASS_$_Fixture", - "0x12": "_OBJC_PROTOCOL_$_FixtureDelegate", - "0x13": "entry", - }), - ), - ); - case "procedure_pseudo_code": { - const procedure = arguments_.procedure; - return Promise.resolve( - ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"), - ); - } - case "procedure_callees": { - const procedure = arguments_.procedure; - return Promise.resolve( - ok( - procedure === "0x1" - ? ["0x2", "0x3"] - : procedure === "0x2" - ? ["0x1"] - : [], - ), - ); - } - case "procedure_callers": - return Promise.resolve(ok(["0x9"])); - case "address_name": - return Promise.resolve(ok(arguments_.address ?? null)); - case "xrefs": - return Promise.resolve(ok(["0x20", "0x21"])); - case "resolve_containing_procedure": - return Promise.resolve( - ok({ - query_address: - typeof arguments_.address === "string" - ? arguments_.address - : "0x0", - found: false, - procedure: null, - reason: "not_in_procedure", - }), - ); - case "list_segments": - return Promise.resolve( - ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]), - ); - case "list_documents": - return Promise.resolve(ok(["fixture"])); - case "list_strings": - return Promise.resolve(ok(inventory({ "0x30": "hello" }))); - case "analyze_function": - return Promise.resolve( - ok({ - procedure: { - address: "0x1", - name: "entry", - signature: null, - locals: [], - }, - pseudocode: "return 0;", - assembly: [], - comments: [], - callers: [], - callees: [], - incoming_references: [], - outgoing_references: [], - referenced_strings: [], - referenced_names: [], - basic_blocks: [], - }), - ); - default: - return Promise.resolve(ok(null)); - } - }, -}); +afterEach(closeEnhancedToolResources); -const resources: Array<{ close(): Promise }> = []; - -afterEach(async () => { - await Promise.all( - resources.splice(0).map(async (resource) => resource.close()), - ); -}); - -const connect = async (analysis: AnalysisOperationPort = fixturePort()) => { - const server = createServer(analysis); - const client = new Client({ name: "enhanced-test", version: "1.0.0" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - resources.push(client, server); - await server.connect(serverTransport); - await client.connect(clientTransport); - return client; -}; - -describe("enhanced MCP tools", () => { - it("rejects incomplete function dossiers at the application boundary", async () => { +describe("analyze_function MCP producer validation", () => { + it("rejects an incomplete function dossier", async () => { const client = await connect({ execute: () => Promise.resolve( - ok({ + observed({ procedure: { address: "0x1", name: "entry" }, pseudocode: { text: "plausible but incomplete" }, }), @@ -127,46 +22,40 @@ describe("enhanced MCP tools", () => { name: "analyze_function", arguments: { procedure: "0x1" }, }); + expect(result.isError).toBe(true); - const text = result.content.find((item) => item.type === "text"); - expect(text?.type === "text" ? text.text : "").toBe( - JSON.stringify(result.structuredContent), - ); + expect(result.content).toContainEqual({ + type: "text", + text: JSON.stringify(result.structuredContent), + }); }); - it("rejects deceptive function dossier collection metadata", async () => { - const malformedPort = fixturePort(); + it("rejects a malformed collection in an otherwise complete dossier", async () => { + const dossier = functionDossierSchema.parse(ghidraFunctionDossier()); const client = await connect({ - execute: async (name, arguments_, options) => { - const result = await malformedPort.execute(name, arguments_, options); - if (!result.ok || name !== "analyze_function") return result; - const dossier = result.value.result; - if ( - typeof dossier !== "object" || - dossier === null || - Array.isArray(dossier) - ) - return result; - return ok({ - ...dossier, - comments: { - items: [], - total: 0, - returned: 1, - truncated: false, - next_offset: null, - }, - }); - }, + execute: () => + Promise.resolve( + observed({ + ...dossier, + comments: { + items: [], + total: 0, + returned: 1, + truncated: false, + next_offset: null, + }, + }), + ), }); const result = await client.callTool({ name: "analyze_function", arguments: { procedure: "0x1" }, }); + expect(result.isError).toBe(true); - const text = result.content.find((item) => item.type === "text"); - expect(text?.type === "text" ? text.text : "").toBe( - JSON.stringify(result.structuredContent), - ); + expect(result.content).toContainEqual({ + type: "text", + text: JSON.stringify(result.structuredContent), + }); }); }); diff --git a/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts b/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts index 8e15ce3d1b..4ae1125d2c 100644 --- a/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts +++ b/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts @@ -12,7 +12,18 @@ afterEach(closeEnhancedToolResources); describe("enhanced MCP tools", () => { it("returns Evidence IDs and a complete call path", async () => { - const client = await connect(); + const client = await connect({ + execute: async (_name, arguments_) => { + const procedure = arguments_.procedure; + return ok( + procedure === "0x1" + ? ["0x2", "0x3"] + : procedure === "0x2" + ? ["0x1"] + : [], + ); + }, + }); const result = await client.callTool({ name: "trace_call_path", arguments: { diff --git a/tests/boundary/mcp/enhancedTools.inputValidation.test.ts b/tests/boundary/mcp/enhancedTools.inputValidation.test.ts index 9f6af2db2c..f6c881c374 100644 --- a/tests/boundary/mcp/enhancedTools.inputValidation.test.ts +++ b/tests/boundary/mcp/enhancedTools.inputValidation.test.ts @@ -1,136 +1,13 @@ -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { afterEach, describe, expect, it } from "vitest"; -import { PROCEDURES, emptyBounded, inventory } from "./enhancedToolsHarness.js"; -import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; -import { createServer } from "../../../src/server/createServer.js"; +import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; -const fixturePort = (): AnalysisOperationPort => ({ - execute: (name, arguments_) => { - switch (name) { - case "list_procedures": - return Promise.resolve(ok(inventory(PROCEDURES))); - case "list_names": - return Promise.resolve( - ok( - inventory({ - "0x10": "_OBJC_CLASS_$_Fixture", - "0x11": "_OBJC_CLASS_$_Fixture", - "0x12": "_OBJC_PROTOCOL_$_FixtureDelegate", - "0x13": "entry", - }), - ), - ); - case "procedure_pseudo_code": { - const procedure = arguments_.procedure; - return Promise.resolve( - ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"), - ); - } - case "procedure_callees": { - const procedure = arguments_.procedure; - return Promise.resolve( - ok( - procedure === "0x1" - ? ["0x2", "0x3"] - : procedure === "0x2" - ? ["0x1"] - : [], - ), - ); - } - case "procedure_callers": - return Promise.resolve(ok(["0x9"])); - case "address_name": - return Promise.resolve(ok(arguments_.address ?? null)); - case "xrefs": - return Promise.resolve(ok(["0x20", "0x21"])); - case "resolve_containing_procedure": - return Promise.resolve( - ok({ - query_address: - typeof arguments_.address === "string" - ? arguments_.address - : "0x0", - found: false, - procedure: null, - reason: "not_in_procedure", - }), - ); - case "list_segments": - return Promise.resolve( - ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]), - ); - case "list_documents": - return Promise.resolve(ok(["fixture"])); - case "list_strings": - return Promise.resolve(ok(inventory({ "0x30": "hello" }))); - case "search_strings": - case "search_procedures": - return Promise.resolve(ok(inventory({ "0x1": "needle" }))); - case "analyze_function": - return Promise.resolve( - ok({ - procedure: { - address: "0x1", - name: "entry", - signature: null, - locals: [], - }, - pseudocode: { - text: "return 0;", - total_chars: 9, - returned_chars: 9, - truncated: false, - next_offset: null, - }, - assembly: { - items: [], - total: 0, - returned: 0, - truncated: false, - next_offset: null, - }, - comments: emptyBounded(), - callers: emptyBounded(), - callees: emptyBounded(), - incoming_references: emptyBounded(), - outgoing_references: emptyBounded(), - referenced_strings: emptyBounded(), - referenced_names: emptyBounded(), - basic_blocks: emptyBounded(), - instruction_scan: { scanned: 0, truncated: false }, - }), - ); - default: - return Promise.resolve(ok(null)); - } - }, -}); - -const resources: Array<{ close(): Promise }> = []; - -afterEach(async () => { - await Promise.all( - resources.splice(0).map(async (resource) => resource.close()), - ); -}); - -const connect = async (analysis: AnalysisOperationPort = fixturePort()) => { - const server = createServer(analysis); - const client = new Client({ name: "enhanced-test", version: "1.0.0" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - resources.push(client, server); - await server.connect(serverTransport); - await client.connect(clientTransport); - return client; -}; +afterEach(closeEnhancedToolResources); describe("enhanced MCP input validation", () => { it("lets the SDK reject misspelled tool inputs", async () => { - const client = await connect(); + const client = await connect({ execute: () => Promise.resolve(ok([])) }); const valid = await client.callTool({ name: "trace_feature", arguments: { query: "needle" }, diff --git a/tests/boundary/mcp/enhancedToolsHarness.ts b/tests/boundary/mcp/enhancedToolsHarness.ts index b4d2b3a6c4..5c4af15dc5 100644 --- a/tests/boundary/mcp/enhancedToolsHarness.ts +++ b/tests/boundary/mcp/enhancedToolsHarness.ts @@ -6,7 +6,6 @@ import { import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; import { createServer } from "../../../src/server/createServer.js"; -import { observed as ok } from "../../fixtures/analysisExecution.js"; export const PROCEDURES = { "0x1": "_TtC7Fixture5Class", @@ -20,119 +19,9 @@ export const PROCEDURES = { export const inventory = (values: Readonly>) => Object.entries(values).map(([address, value]) => ({ address, value })); -export const fixturePort = (): AnalysisOperationPort => ({ - execute: (name, arguments_) => { - switch (name) { - case "list_procedures": - return Promise.resolve(ok(inventory(PROCEDURES))); - case "list_names": - return Promise.resolve( - ok( - inventory({ - "0x10": "_OBJC_CLASS_$_Fixture", - "0x11": "_OBJC_CLASS_$_Fixture", - "0x12": "_OBJC_PROTOCOL_$_FixtureDelegate", - "0x13": "entry", - }), - ), - ); - case "procedure_pseudo_code": { - const procedure = arguments_.procedure; - return Promise.resolve( - ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"), - ); - } - case "procedure_callees": { - const procedure = arguments_.procedure; - return Promise.resolve( - ok( - procedure === "0x1" - ? ["0x2", "0x3"] - : procedure === "0x2" - ? ["0x1"] - : [], - ), - ); - } - case "procedure_callers": - return Promise.resolve(ok(["0x9"])); - case "address_name": - return Promise.resolve(ok(arguments_.address ?? null)); - case "xrefs": - return Promise.resolve(ok(["0x20", "0x21"])); - case "resolve_containing_procedure": - return Promise.resolve( - ok({ - query_address: - typeof arguments_.address === "string" - ? arguments_.address - : "0x0", - found: false, - procedure: null, - reason: "not_in_procedure", - }), - ); - case "list_segments": - return Promise.resolve( - ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]), - ); - case "list_documents": - return Promise.resolve(ok(["fixture"])); - case "list_strings": - return Promise.resolve(ok(inventory({ "0x30": "hello" }))); - case "analyze_function": - return Promise.resolve( - ok({ - procedure: { - address: "0x1", - name: "entry", - signature: null, - locals: [], - }, - pseudocode: { - text: "return 0;", - total_chars: 9, - returned_chars: 9, - truncated: false, - next_offset: null, - }, - assembly: { - items: [], - total: 0, - returned: 0, - truncated: false, - next_offset: null, - }, - comments: emptyBounded(), - callers: emptyBounded(), - callees: emptyBounded(), - incoming_references: emptyBounded(), - outgoing_references: emptyBounded(), - referenced_strings: emptyBounded(), - referenced_names: emptyBounded(), - basic_blocks: emptyBounded(), - instruction_scan: { scanned: 0, truncated: false }, - }), - ); - default: - return Promise.resolve(ok(null)); - } - }, -}); - -export const emptyBounded = () => ({ - items: [], - total: 0, - returned: 0, - truncated: false, - next_offset: null, -}); - export const resources: Array<{ close(): Promise }> = []; -export const connect = async ( - analysis: AnalysisOperationPort = fixturePort(), -) => { +export const connect = async (analysis: AnalysisOperationPort) => { const server = createServer(analysis); const client = new Client({ name: "enhanced-test", version: "1.0.0" }); const [clientTransport, serverTransport] = diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts new file mode 100644 index 0000000000..1047b34592 --- /dev/null +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -0,0 +1,78 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it, onTestFinished } from "vitest"; +import { EvmInterfaceService } from "../../../src/application/evm/EvmInterfaceService.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + evmInterfaceFixture, + evmInterfaceExecution, +} from "../../fixtures/evm/interface.js"; + +it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => { + const value = evmInterfaceFixture(); + const service = new EvmInterfaceService({ + inspect: () => Promise.resolve(ok(evmInterfaceExecution(value))), + }); + const session = createTestBinarySession(() => { + throw new Error("deep provider must not start"); + }); + const server = createServer(session, session, { evmInterface: service }); + const client = new Client({ name: "evm-interface-contract", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = (await client.listTools()).tools.find( + (tool) => tool.name === "inspect_evm_interface", + ); + if (advertised?.outputSchema === undefined) + throw new Error("EVM interface inspection must publish both schemas"); + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + const inputSchema: Record = advertised.inputSchema; + const outputSchema: Record = advertised.outputSchema; + expect(ajv.validateSchema(inputSchema)).toBe(true); + expect(ajv.validateSchema(outputSchema)).toBe(true); + expect( + ajv.validate(inputSchema, { path: value.artifact.path, encoding: "hex" }), + ).toBe(true); + expect( + ajv.validate(inputSchema, { + path: value.artifact.path, + encoding: "hex", + approval: true, + }), + ).toBe(false); + const response = await client.callTool({ + name: "inspect_evm_interface", + arguments: { path: value.artifact.path, encoding: "hex" }, + }); + expect(response.isError).not.toBe(true); + expect( + ajv.validate(outputSchema, response.structuredContent), + JSON.stringify(ajv.errors), + ).toBe(true); + const parsed = toolContract("inspect_evm_interface").outputSchema.parse( + response.structuredContent, + ); + const evidence = parseEvidence(parsed.evidence); + expect(parsed.result).toEqual(value); + expect(parsed.result).toEqual(evidence.normalized_result); + expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence); + expect( + ( + await client.callTool({ + name: "inspect_evm_interface", + arguments: { path: "relative.hex", encoding: "hex" }, + }) + ).isError, + ).toBe(true); +}); diff --git a/tests/boundary/mcp/mainErrors.test.ts b/tests/boundary/mcp/mainErrors.test.ts index 3625b1974a..0b3e3ce44b 100644 --- a/tests/boundary/mcp/mainErrors.test.ts +++ b/tests/boundary/mcp/mainErrors.test.ts @@ -124,28 +124,6 @@ describe("MCP runtime errors", () => { ]); }); - it("starts the core transport when optional providers fail to load", async () => { - const output: string[] = []; - let serveCalls = 0; - expect( - await run( - dependencies({ - serve: () => { - serveCalls += 1; - return { close: () => Promise.resolve() }; - }, - output, - shutdown: [], - exitCodes: [], - loadOptionalProviders: () => - Promise.reject(new Error("synthetic optional import failure")), - }), - ), - ).toBe(0); - expect(serveCalls).toBe(1); - expect(output).toEqual([]); - }); - it("reports shutdown failure and sets a failing exit code", async () => { const output: string[] = []; const shutdown: Array<() => void> = []; diff --git a/tests/boundary/mcp/mainReload.test.ts b/tests/boundary/mcp/mainReload.test.ts index ed5a65c359..e101cf6095 100644 --- a/tests/boundary/mcp/mainReload.test.ts +++ b/tests/boundary/mcp/mainReload.test.ts @@ -8,7 +8,7 @@ import type { RuntimeDependencies } from "../../../src/main/types.js"; import { createRuntimeState } from "../../../src/main/state.js"; describe("runtime configuration reload", () => { - it("applies a valid configuration reload", () => { + it("applies valid reloads and keeps the last valid config after an invalid reload", () => { const env: NodeJS.ProcessEnv = { REA_LOG_LEVEL: "info" }; const runtime = setupReload(env); @@ -16,16 +16,11 @@ describe("runtime configuration reload", () => { runtime.reload(); expect(runtime.state.currentConfig.logLevel).toBe("debug"); - }); - - it("retains the last valid configuration after an invalid reload", () => { - const env: NodeJS.ProcessEnv = { REA_LOG_LEVEL: "info" }; - const runtime = setupReload(env); env.REA_LOG_LEVEL = "invalid"; runtime.reload(); - expect(runtime.state.currentConfig.logLevel).toBe("info"); + expect(runtime.state.currentConfig.logLevel).toBe("debug"); }); }); diff --git a/tests/boundary/mcp/managedMcp.test.ts b/tests/boundary/mcp/managedMcp.test.ts index 4ededb36de..f54aa37ac9 100644 --- a/tests/boundary/mcp/managedMcp.test.ts +++ b/tests/boundary/mcp/managedMcp.test.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import type { CallToolResult } from "@modelcontextprotocol/server"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it } from "vitest"; import { z } from "zod"; @@ -14,16 +15,21 @@ import type { BinarySession } from "../../../src/application/binary/BinarySessio import type { BinaryTarget } from "../../../src/domain/binaryTarget.js"; import { SessionProviderRouter } from "../../../src/application/binary/SessionProviderRouter.js"; import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../../src/contracts/managed/managedWorkflowExamples.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js"; import { createServer } from "../../../src/server/createServer.js"; import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; +type ManagedOutputAssertion = Awaited>; + it("runs every managed static inspection independently of an active native target", async () => { const directory = await createTestTempDirectory( "rea-managed-independent-mcp-", ); const path = join(directory, "fixture.dll"); + const otherPath = join(directory, "other-fixture.dll"); await writeFile(path, buildManagedPeFixture()); + await writeFile(otherPath, buildManagedPeFixture({ methodName: "Other" })); const session = composeBinarySession( SessionProviderRouter.selectable(new AnalysisProviderRegistry([]), [ new ManagedStaticProvider(), @@ -50,6 +56,12 @@ it("runs every managed static inspection independently of an active native targe try { await server.connect(serverTransport); await client.connect(clientTransport); + const managedMembersTool = (await client.listTools()).tools.find( + ({ name }) => name === "inspect_managed_members", + ); + expect(managedMembersTool?.inputSchema).toMatchObject({ + additionalProperties: false, + }); const artifact = structured( await client.callTool({ name: "inspect_managed_artifact", @@ -63,6 +75,11 @@ it("runs every managed static inspection independently of an active native targe subject: { local_path: path, format: "pe" }, }, }); + const misspelledPath = await client.callTool({ + name: "inspect_managed_members", + arguments: { pth: otherPath }, + }); + expect(misspelledPath.isError).toBe(true); const invalidSelection = await client.callTool({ name: "inspect_managed_artifact", arguments: { path: join(directory, "missing.dll") }, @@ -78,6 +95,24 @@ it("runs every managed static inspection independently of an active native targe subject: { local_path: path, format: "pe" }, }, }); + const otherArtifact = structured( + await client.callTool({ + name: "inspect_managed_artifact", + arguments: { path: otherPath }, + }), + ); + expect(otherArtifact).toMatchObject({ + evidence: { subject: { local_path: otherPath, format: "pe" } }, + }); + const reusedOtherTarget = structured( + await client.callTool({ name: "inspect_managed_members", arguments: {} }), + ); + expect(reusedOtherTarget).toMatchObject({ + evidence: { + operation: "inspect_managed_members", + subject: { local_path: otherPath, format: "pe" }, + }, + }); const boundaries = structured( await client.callTool({ name: "inspect_managed_native_boundaries", @@ -126,9 +161,19 @@ it("opens a managed PE and executes the managed static provider through MCP", as try { await server.connect(serverTransport); await client.connect(clientTransport); - await verifyManagedCatalogAndNativeWorkflow(client, session); - const members = await inspectManagedStaticWorkflow(client, path); - await verifyManagedComparisonAndReconstruction(client, members, rightPath); + const assertOutput = await createOutputAssertion(client); + await verifyManagedCatalogAndNativeWorkflow(client, session, assertOutput); + const members = await inspectManagedStaticWorkflow( + client, + path, + assertOutput, + ); + await verifyManagedComparisonAndReconstruction( + client, + members, + rightPath, + assertOutput, + ); } finally { await Promise.all([client.close(), server.close()]); await session.close(); @@ -138,9 +183,9 @@ it("opens a managed PE and executes the managed static provider through MCP", as const verifyManagedCatalogAndNativeWorkflow = async ( client: Client, session: BinarySession, + outputAssertion: ManagedOutputAssertion, ): Promise => { - const names = (await client.listTools()).tools.map(({ name }) => name); - expect(names).toEqual( + expect(outputAssertion.names).toEqual( expect.arrayContaining([ "inspect_managed_artifact", "inspect_managed_members", @@ -166,6 +211,7 @@ const verifyManagedCatalogAndNativeWorkflow = async ( }, }), ); + await outputAssertion.assert("verify_managed_native_boundaries", verified); expect(verified).toMatchObject({ evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u), result: { @@ -195,6 +241,7 @@ const verifyManagedCatalogAndNativeWorkflow = async ( const inspectManagedStaticWorkflow = async ( client: Client, path: string, + outputAssertion: ManagedOutputAssertion, ): Promise> => { const inspected = structured( await client.callTool({ @@ -202,6 +249,7 @@ const inspectManagedStaticWorkflow = async ( arguments: { path }, }), ); + await outputAssertion.assert("inspect_managed_artifact", inspected); expect(inspected).toMatchObject({ result: { classification: { status: "managed", runtime_family: "modern-dotnet" }, @@ -221,6 +269,11 @@ const inspectManagedStaticWorkflow = async ( }), ), ); + await outputAssertion.assert("inspect_managed_members", { + result: members.normalized_result, + evidence_id: members.evidence_id, + evidence: members, + }); expect(members).toMatchObject({ operation: "inspect_managed_members", provider: { id: "rea-dotnet-static" }, @@ -242,6 +295,7 @@ const inspectManagedStaticWorkflow = async ( arguments: { path }, }), ); + await outputAssertion.assert("inspect_managed_native_boundaries", boundaries); expect(boundaries).toMatchObject({ result: { identity_scope: { token_identity: "build-local" }, @@ -292,6 +346,7 @@ const verifyManagedComparisonAndReconstruction = async ( client: Client, members: Record, rightPath: string, + outputAssertion: ManagedOutputAssertion, ): Promise => { await client.callTool({ name: "open_binary", @@ -316,6 +371,11 @@ const verifyManagedComparisonAndReconstruction = async ( }), ), ); + await outputAssertion.assert("compare_managed_members", { + result: compared.normalized_result, + evidence_id: compared.evidence_id, + evidence: compared, + }); expect(compared).toMatchObject({ operation: "compare_managed_members", provider: { id: "rea-dotnet-workflows" }, @@ -394,6 +454,31 @@ const structured = (result: CallToolResult): Record => { return z.record(z.string(), z.unknown()).parse(result.structuredContent); }; +const createOutputAssertion = async (client: Client) => { + const tools = (await client.listTools()).tools; + const ajv = new Ajv2020({ + strict: false, + validateFormats: false, + }); + return { + names: tools.map(({ name }) => name), + assert: ( + name: Parameters[0], + output: Record, + ): void => { + const contract = toolContract(name); + const wire = tools.find((tool) => tool.name === name); + if (wire?.outputSchema === undefined) + throw new Error(`Missing advertised output schema for ${name}`); + const valid = ajv.compile( + z.record(z.string(), z.unknown()).parse(wire.outputSchema), + ); + expect(valid(output), `${name} advertised output`).toBe(true); + expect(contract.outputSchema.safeParse(output).success).toBe(true); + }, + }; +}; + const inlineEvidence = ( value: Readonly>, ): Record => { diff --git a/tests/boundary/mcp/mcpStartupPolicy.test.ts b/tests/boundary/mcp/mcpStartupPolicy.test.ts index ce37066706..da38caa197 100644 --- a/tests/boundary/mcp/mcpStartupPolicy.test.ts +++ b/tests/boundary/mcp/mcpStartupPolicy.test.ts @@ -1,25 +1,11 @@ import { describe, expect, it } from "vitest"; -import { MCP_STARTUP_POLICY } from "../../../src/mcpStartupPolicy.js"; import { createEsmModuleProfileCollector, parseLinuxVmRssBytes, } from "../../../scripts/lib/mcp-startup-probe.mjs"; describe("MCP startup policy", () => { - it("preserves headroom beneath the configured Codex timeout", () => { - const codexTimeoutMs = - MCP_STARTUP_POLICY.codexStartupTimeoutSeconds * 1_000; - - expect(MCP_STARTUP_POLICY.initializeBudgetMs).toBeLessThan(codexTimeoutMs); - expect(MCP_STARTUP_POLICY.firstCatalogBudgetMs).toBeLessThan( - codexTimeoutMs, - ); - expect(MCP_STARTUP_POLICY.doctorDeadlineMs).toBeGreaterThanOrEqual( - codexTimeoutMs, - ); - }); - it("parses Linux resident memory without accepting other fields", () => { expect( parseLinuxVmRssBytes("Name:\tnode\nVmRSS:\t 12345 kB\nThreads:\t7\n"), diff --git a/tests/boundary/mcp/nativeCallObservationMcp.test.ts b/tests/boundary/mcp/nativeCallObservationMcp.test.ts new file mode 100644 index 0000000000..ba16770dac --- /dev/null +++ b/tests/boundary/mcp/nativeCallObservationMcp.test.ts @@ -0,0 +1,114 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { nativeCallObservationResultSchema } from "../../../src/domain/native/nativeCallObservation.js"; +import { ok } from "../../../src/domain/result.js"; +import type { NativeCallTracer } from "../../../src/native/LldbCallTracer.js"; +import { NativeMacOSProvider } from "../../../src/native/NativeMacOSProvider.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { NativeFixtureRunner } from "../../fixtures/nativeCommands.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +/** A thin little-endian arm64 MH_EXECUTE header with no load commands. */ +const machoHeader = (): Buffer => { + const header = Buffer.alloc(32); + header.writeUInt32LE(0xfeedfacf, 0); + header.writeUInt32LE(0x0100000c, 4); + header.writeUInt32LE(2, 12); + return header; +}; + +const tracer: NativeCallTracer & { calls: number } = { + calls: 0, + trace(request) { + this.calls++; + return Promise.resolve( + ok({ + debugger: { + path: "/usr/bin/lldb", + sha256: "b".repeat(64), + version: "lldb-fixture", + }, + run: { + status: "traced", + version: "lldb-fixture", + pid: 99, + outcome: "exited", + exit_status: 0, + exit_description: null, + killed: false, + terminated: true, + elapsed_ms: 1, + breakpoints: request.input.breakpoints.map((_, index) => ({ + index, + location_count: 1, + locations: [], + })), + events: [], + other_stops: [], + }, + stdout: { text: "", bytes: 0, truncated: false }, + stderr: { text: "", bytes: 0, truncated: false }, + terminated: true, + }), + ); + }, +}; + +it("routes observe_native_calls through MCP with schema-checked input and output", async () => { + const directory = await createTestTempDirectory("rea-native-calls-mcp-"); + const path = join(directory, "Tool"); + await writeFile(path, machoHeader()); + const session = createTestBinarySession( + new NativeMacOSProvider(new NativeFixtureRunner(), "darwin", tracer), + ); + const server = createServer(session, session); + const client = new Client({ name: "native-calls-mcp-test", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const opened = await client.callTool({ + name: "open_binary", + arguments: { path }, + }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const called = await client.callTool({ + name: "observe_native_calls", + arguments: { + breakpoints: [{ kind: "function", name: "open" }], + duration_ms: 1000, + }, + }); + expect(called.isError, JSON.stringify(called.structuredContent)).not.toBe( + true, + ); + const result = nativeCallObservationResultSchema.parse( + z.object({ result: z.unknown() }).parse(called.structuredContent).result, + ); + expect(result.process).toMatchObject({ pid: 99, outcome: "exited" }); + expect(result.breakpoints[0]?.request).toEqual({ + kind: "function", + name: "open", + module: null, + }); + const rejected = await client.callTool({ + name: "observe_native_calls", + arguments: { + breakpoints: [{ kind: "function", name: "open" }], + duration_ms: 1, + }, + }); + expect(rejected.isError).toBe(true); + expect(tracer.calls).toBe(1); + } finally { + await client.close(); + await server.close(); + } +}); diff --git a/tests/boundary/mcp/nativeInputContracts.test.ts b/tests/boundary/mcp/nativeInputContracts.test.ts new file mode 100644 index 0000000000..4429f88639 --- /dev/null +++ b/tests/boundary/mcp/nativeInputContracts.test.ts @@ -0,0 +1,119 @@ +import { Ajv2020 } from "ajv/dist/2020.js"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { afterEach, describe, expect, it } from "vitest"; + +import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; +import { NATIVE_TOOL_CONTRACTS } from "../../../src/contracts/native/nativeToolContracts.js"; +import type { JsonValue } from "../../../src/domain/jsonValue.js"; +import { ok } from "../../../src/domain/result.js"; +import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; +import { createServer } from "../../../src/server/createServer.js"; + +const provider = { + id: "fixture-native", + name: "Fixture native provider", + version: "1", +} as const; + +const plistResult = { + format: "xml", + value: {}, + bundle: { + identifier: null, + executable: null, + name: null, + version: null, + short_version: null, + }, + source_path: "/Applications/Fixture.app/Contents/Info.plist", + provenance: [], + limitations: [], +}; + +const resources: Array<{ close(): Promise }> = []; + +const connect = async ( + execute: AnalysisOperationPort["execute"], +): Promise => { + const server = createServer({ execute }); + const client = new Client({ name: "native-contract-test", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + resources.push(client, server); + await server.connect(serverTransport); + await client.connect(clientTransport); + return client; +}; + +afterEach(async () => { + await Promise.all( + resources.splice(0).map(async (resource) => resource.close()), + ); +}); + +describe("native MCP input contracts", () => { + it("rejects a misspelled plist path before dispatch and preserves omission", async () => { + const calls: Array<{ + readonly operation: string; + readonly parameters: Readonly>; + }> = []; + const client = await connect((operation, parameters) => { + calls.push({ operation, parameters }); + return Promise.resolve( + ok(createAnalysisExecution(plistResult, provider)), + ); + }); + + const tools = new Map( + (await client.listTools()).tools.map((tool) => [tool.name, tool]), + ); + for (const nativeContract of NATIVE_TOOL_CONTRACTS) + expect(tools.get(nativeContract.name)?.inputSchema).toHaveProperty( + "additionalProperties", + false, + ); + + const contract = NATIVE_TOOL_CONTRACTS.find( + ({ name }) => name === "inspect_plist", + ); + const advertised = tools.get("inspect_plist"); + if (contract === undefined || advertised === undefined) + throw new Error("inspect_plist was not registered"); + + const validate = new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(advertised.inputSchema); + const typoInput = { pth: "/tmp/Other.plist" }; + expect(validate(typoInput)).toBe(false); + expect(contract.inputSchema.safeParse(typoInput).success).toBe(false); + expect(validate({})).toBe(true); + expect(contract.inputSchema.safeParse({}).success).toBe(true); + + const defaulted = await client.callTool({ + name: "inspect_plist", + arguments: {}, + }); + expect(defaulted.isError).not.toBe(true); + expect(calls[0]).toEqual({ operation: "inspect_plist", parameters: {} }); + + const selectedPath = "/tmp/Other.plist"; + const explicit = await client.callTool({ + name: "inspect_plist", + arguments: { path: selectedPath }, + }); + expect(explicit.isError).not.toBe(true); + expect(calls[1]).toEqual({ + operation: "inspect_plist", + parameters: { path: selectedPath }, + }); + + const beforeTypo = calls.length; + const rejected = await client.callTool({ + name: "inspect_plist", + arguments: typoInput, + }); + expect(rejected.isError).toBe(true); + expect(calls).toHaveLength(beforeTypo); + }); +}); diff --git a/tests/boundary/mcp/officialTools.test.ts b/tests/boundary/mcp/officialTools.test.ts index ce95aa0c3a..b7e4f00145 100644 --- a/tests/boundary/mcp/officialTools.test.ts +++ b/tests/boundary/mcp/officialTools.test.ts @@ -1,5 +1,4 @@ -import { functionDossierSchema } from "../../../src/domain/hopperValues.js"; -import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { afterEach, describe, expect, it } from "vitest"; @@ -11,52 +10,6 @@ import { observed as ok } from "../../fixtures/analysisExecution.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; import { createServer } from "../../../src/server/createServer.js"; -const VALID_INPUTS: Readonly< - Record>> -> = { - annotate_native_function: { procedure: "0x1000", name: "entry" }, - inspect_native_load_image: {}, - address_name: {}, - inspect_native_instruction: { address: "0x1000" }, - resolve_native_call_targets: { address: "0x1000" }, - inspect_native_data_type: { type: "/Missing" }, - comment: {}, - current_address: {}, - current_procedure: {}, - current_document: {}, - goto_address: { address: "0x1000" }, - inline_comment: {}, - list_bookmarks: {}, - list_documents: {}, - list_names: {}, - list_procedures: {}, - list_segments: {}, - list_strings: {}, - next_address: {}, - prev_address: {}, - procedure_address: { procedure: "main" }, - procedure_assembly: { procedure: "main" }, - procedure_callees: { procedure: "main" }, - procedure_callers: { procedure: "main" }, - procedure_info: { procedure: "main" }, - read_function_instructions: { procedure: "main" }, - read_bytes: { address: "0x1000", length: 4 }, - address_to_file_offset: { address: "0x1000" }, - procedure_references: { procedure: "main" }, - procedure_pseudo_code: { procedure: "main" }, - resolve_containing_procedure: { address: "0x1000" }, - search_procedures: { pattern: "main" }, - search_strings: { pattern: "hello" }, - set_address_name: { address: "0x1000", name: "entry" }, - set_addresses_names: { names: { "0x1000": "entry" } }, - set_bookmark: { address: "0x1000" }, - set_comment: { address: "0x1000", comment: "entry point" }, - set_current_document: { document: "fixture" }, - set_inline_comment: { address: "0x1000", comment: "entry point" }, - unset_bookmark: { address: "0x1000" }, - xrefs: {}, -}; - interface Invocation { readonly name: string; readonly arguments_: Readonly>; @@ -128,204 +81,54 @@ const connect = async (analysis: AnalysisOperationPort) => { return client; }; -const inventory: JsonValue = []; - -const outputFor = (name: string): JsonValue => { - if (name === "annotate_native_function") { - const dossier = functionDossierSchema.parse(ghidraFunctionDossier()); - return { - annotations: { - address: dossier.procedure.address, - name: dossier.procedure.name, - comment: null, - inline_comment: null, - }, - dossier, - effects: { - scope: "session-analysis-database", - source_bytes_modified: false, - persists_after_close: false, - }, - }; - } - if (name === "inspect_native_load_image") - return { - status: "unsupported", - reason: "Fixture provider has no load-image verifier", - observations: { - executable_format: "ELF", - language_id: "x86:LE:64:default", - compiler_spec_id: "gcc", - image_base: "0x0", - default_address_space: "ram", - source_files: [], - mappings: [], - relocations: [], - entry_context: [], - entry_points: [], - }, - limitations: ["Transport fixture only"], - }; - if (name === "inspect_native_instruction") - return { - address: "0x1000", - status: "decoded", - procedure: "0x1000", - architecture: "x86:LE:64:default", - mode: "default", - bytes: "c3", - length: 1, - mnemonic: "RET", - raw_disassembly: "RET", - operands: [], - flow: { - kind: "terminal", - conditional: false, - computed: false, - direct_destinations: [], - }, - references: [], - limitations: ["Provider decoder observation"], - }; - if (name === "resolve_native_call_targets") - return { - call_site: "0x1000", - procedure: "0x1000", - status: "not-call", - mechanism: "unavailable", - targets: [], - limitations: [], - }; - if (name === "inspect_native_data_type") - return { - status: "unavailable", - reason: "Type absent", - id: null, - name: null, - kind: "unavailable", - source: "analysis-database", - source_archive: null, - address: null, - size_bytes: null, - alignment_bytes: null, - packing_enabled: null, - referenced_type: null, - array_count: null, - array_stride_bytes: null, - fields: [], - members: [], - total_fields: 0, - truncated: false, - limitations: [], - }; - if (["list_procedures", "list_names", "list_strings"].includes(name)) - return inventory; - if (["address_name", "comment", "inline_comment"].includes(name)) return null; - if (["procedure_callees", "procedure_callers", "xrefs"].includes(name)) - return []; - if (["list_bookmarks", "list_documents", "list_segments"].includes(name)) - return []; - if ( - [ - "set_address_name", - "set_bookmark", - "set_comment", - "set_inline_comment", - "unset_bookmark", - ].includes(name) - ) - return true; - if (name === "set_addresses_names") return { "0x1000": true }; - if (["search_procedures", "search_strings"].includes(name)) return inventory; - if (name === "procedure_info") - return { - name: "main", - entrypoint: "0x1000", - basicblock_count: 1, - length: 4, - signature: null, - locals: [], - }; - if (name === "resolve_containing_procedure") - return { - query_address: "0x1000", - found: true, - procedure: { address: "0x1000", name: "main" }, - }; - if (name === "read_function_instructions") - return { - procedure: { address: "0x1000", name: "main" }, - instructions: ["0x1000: ret"], - limitations: ["Provider-specific instruction text."], - }; - if (name === "read_bytes") - return { - address: "0x1000", - requested_bytes: 4, - returned_bytes: 4, - bytes_hex: "c3c3c3c3", - complete: true, - }; - if (name === "address_to_file_offset") - return { address: "0x1000", file_offset: 0 }; - if (name === "procedure_references") - return { - procedure: { address: "0x1000", name: "main" }, - direction: "outgoing", - references: [], - }; - return name.includes("address") ? "0x1000" : "fixture"; -}; - -describe("official Hopper proxy tools", () => { - it("executes every handler and projects omitted Python optionals to null", async () => { +describe("official tool input contracts", () => { + it("rejects misspelled top-level inputs before dispatch and keeps record keys open", async () => { const invocations: Invocation[] = []; const client = await connect({ execute: (name, arguments_) => { invocations.push({ name, arguments_ }); - return Promise.resolve(ok(outputFor(name))); + return Promise.resolve(ok(null)); }, }); - + const { tools } = await client.listTools(); + const advertisedByName = new Map(tools.map((tool) => [tool.name, tool])); for (const contract of OFFICIAL_TOOL_CONTRACTS) { - const result = await client.callTool({ - name: contract.name, - arguments: VALID_INPUTS[contract.name], - }); - expect(result.isError).not.toBe(true); + expect(advertisedByName.get(contract.name)?.inputSchema).toHaveProperty( + "additionalProperties", + false, + ); } + const advertised = tools.find(({ name }) => name === "address_name"); + const recordTool = tools.find(({ name }) => name === "set_addresses_names"); + if (advertised === undefined || recordTool === undefined) + throw new Error("Official tools were not advertised"); - expect( - invocations.find(({ name }) => name === "annotate_native_function") - ?.arguments_, - ).toEqual({ procedure: "0x1000", name: "entry" }); - expect(invocations.map(({ name }) => name)).toEqual( - OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name), + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + const validateAddressName = ajv.compile(advertised.inputSchema); + const validateRecordInput = ajv.compile(recordTool.inputSchema); + const typoInput = { adress: "0x1234" }; + expect(advertised.inputSchema).toHaveProperty( + "additionalProperties", + false, ); + expect(validateAddressName(typoInput)).toBe(false); expect( - invocations.find(({ name }) => name === "address_name")?.arguments_, - ).toEqual({ - document: null, - address: null, - }); - expect( - invocations.find(({ name }) => name === "search_procedures")?.arguments_, - ).toEqual({ - pattern: "main", - mode: "literal", - case_sensitive: false, - document: null, - }); - expect( - invocations.find(({ name }) => name === "search_strings")?.arguments_, - ).toEqual({ - pattern: "hello", - mode: "literal", - case_sensitive: false, - document: null, - }); - }); + OFFICIAL_TOOL_CONTRACTS.find( + ({ name }) => name === "address_name", + )?.inputSchema.safeParse(typoInput).success, + ).toBe(false); + expect(validateRecordInput({ names: { "0x1000": "entry" } })).toBe(true); + const result = await client.callTool({ + name: "address_name", + arguments: typoInput, + }); + expect(result.isError).toBe(true); + expect(invocations).toEqual([]); + }); +}); + +describe("official Hopper proxy tools", () => { it("returns stable safe MCP error content", async () => { const client = await connect({ execute: () => Promise.resolve(err(new HopperRemoteError(-1, "denied"))), @@ -341,7 +144,7 @@ describe("official Hopper proxy tools", () => { }); }); - it("returns every procedure inline without caller-authored page calls", async () => { + it("binds list_procedures and projects its omitted Python document option", async () => { const invocations: Invocation[] = []; const client = await connect({ execute: (name, arguments_) => { @@ -367,6 +170,7 @@ describe("official Hopper proxy tools", () => { ], }); expect(invocations).toHaveLength(1); + expect(invocations[0]?.name).toBe("list_procedures"); expect(invocations[0]?.arguments_).toEqual({ document: null }); expect( (await client.listTools()).tools.find( diff --git a/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts b/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts new file mode 100644 index 0000000000..84d87dfea3 --- /dev/null +++ b/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts @@ -0,0 +1,166 @@ +import { createHash } from "node:crypto"; + +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../../src/contracts/managed/managedWorkflowExamples.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; +import { + managedNativeVerificationInputSchema, + managedNativeVerificationResultSchema, + verifyManagedNativeBoundaries, +} from "../../../src/domain/managed/managedNativeVerification.js"; +import { nativeUiResultSchema } from "../../../src/domain/native/nativeUiObservation.js"; +import { processCaptureSchema } from "../../../src/domain/process/processCapture.js"; +import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; + +const record = (value: unknown): Record => { + if (typeof value !== "object" || value === null || Array.isArray(value)) + throw new Error("Expected a schema object"); + return value as Record; +}; + +it("advertises semantic output invariants and keeps producers aligned with Zod", async () => { + const contracts = [ + toolContract("capture_native_ui_scenario"), + toolContract("capture_process_scenario"), + toolContract("verify_managed_native_boundaries"), + ]; + const server = new McpServer({ + name: "output-schema-semantics", + version: "1", + }); + const client = new Client({ name: "output-schema-semantics", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + for (const contract of contracts) + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + async () => ({ content: [] }), + ); + + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = new Map( + (await client.listTools()).tools.map((tool) => [tool.name, tool]), + ); + const outputResultSchema = (name: (typeof contracts)[number]["name"]) => { + const tool = advertised.get(name); + if (tool?.outputSchema === undefined) + throw new Error(`${name} output schema missing`); + const outputSchema = record(tool.outputSchema); + const result = record(record(outputSchema.properties).result); + const standaloneResultSchema = z.record(z.string(), z.unknown()).parse({ + ...result, + ...(typeof outputSchema.$schema === "string" + ? { $schema: outputSchema.$schema } + : {}), + ...(outputSchema.$defs === undefined + ? {} + : { $defs: outputSchema.$defs }), + }); + return { + result, + validate: new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(standaloneResultSchema), + }; + }; + + const native = outputResultSchema("capture_native_ui_scenario"); + const initial = record(record(native.result.properties).initial); + const screenshotUnion = record(record(initial.properties).screenshot); + const screenshotVariants = screenshotUnion.anyOf; + if (!Array.isArray(screenshotVariants)) + throw new Error("Expected nullable screenshot schema"); + const screenshotProperties = record( + record(screenshotVariants[0]).properties, + ); + expect(record(screenshotProperties.base64).description).toContain( + "PNG signature", + ); + expect(record(screenshotProperties.sha256).description).toContain( + "decoded PNG bytes", + ); + const pngBytes = Buffer.from("not a PNG"); + const invalidScreenshot = { + target_sha256: "a".repeat(64), + initial: { + window: { + pid: 1, + window_id: 1, + executable: "app", + launch_time: 0, + title: "", + }, + nodes: [], + truncated: false, + screenshot: { + mime_type: "image/png", + base64: pngBytes.toString("base64"), + sha256: createHash("sha256").update(pngBytes).digest("hex"), + width: 1, + height: 1, + }, + gaps: [], + }, + steps: [], + restore: "leave-as-is", + limitations: [], + }; + expect(native.validate(invalidScreenshot)).toBe(true); + expect(nativeUiResultSchema.safeParse(invalidScreenshot).success).toBe( + false, + ); + + const process = outputResultSchema("capture_process_scenario"); + expect(process.result.description).toContain("canonical scenario"); + expect(process.result.description).toContain( + "older input without an event journal", + ); + const validCapture = processCaptureSchema.parse( + EMPTY_PROCESS_CAPTURE_EXAMPLE, + ); + expect(validCapture.event_journal).toEqual([]); + expect(process.validate(validCapture)).toBe(true); + expect( + processCaptureSchema.safeParse(EMPTY_PROCESS_CAPTURE_EXAMPLE).success, + ).toBe(true); + const invalidCapture = { + ...validCapture, + manifest: { + ...validCapture.manifest, + full_scenario_sha256: "0".repeat(64), + }, + }; + expect(process.validate(invalidCapture)).toBe(true); + expect(processCaptureSchema.safeParse(invalidCapture).success).toBe(false); + + const managed = outputResultSchema("verify_managed_native_boundaries"); + expect(managed.result.description).toContain( + "accepted plus unsupported equals total", + ); + const validVerification = verifyManagedNativeBoundaries( + managedNativeVerificationInputSchema.parse( + MANAGED_NATIVE_VERIFICATION_EXAMPLE, + ), + ); + expect(managed.validate(validVerification)).toBe(true); + const invalidVerification = structuredClone(validVerification); + invalidVerification.summary.verified += 1; + expect(managed.validate(invalidVerification)).toBe(true); + expect( + managedNativeVerificationResultSchema.safeParse(invalidVerification) + .success, + ).toBe(false); + } finally { + await Promise.allSettled([client.close(), server.close()]); + } +}); diff --git a/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts b/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts index 2567976faa..c4b709a32e 100644 --- a/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts +++ b/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts @@ -5,14 +5,14 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it, onTestFinished } from "vitest"; import { z } from "zod"; -import { compareProcessEvidenceFiles } from "../../../src/application/ProcessCli.js"; -import { PROCESS_PROVIDER } from "../../../src/application/ProcessEvidence.js"; +import { compareProcessEvidenceFiles } from "../../../src/application/process/ProcessCli.js"; +import { PROCESS_PROVIDER } from "../../../src/application/process/ProcessEvidence.js"; import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js"; import { FUNCTION_COMPARISON_EVIDENCE, INVESTIGATION_EXAMPLES, } from "../../../src/contracts/investigationExamples.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/process/processCaptureExample.js"; import { findChangedBehavior } from "../../../src/domain/changedBehavior.js"; import { createEvidence, @@ -31,12 +31,15 @@ const providerNames = [ "REA deterministic process harness", ]; -const captureEvidence = (side: "left" | "right") => +const captureEvidence = ( + side: "left" | "right", + result: Evidence["normalized_result"] = EMPTY_PROCESS_CAPTURE_EXAMPLE, +) => createEvidence(undefined, PROCESS_PROVIDER, { predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: { side }, - result: EMPTY_PROCESS_CAPTURE_EXAMPLE, + result, confidence: "observed", authority: "controlled-replay", environment: { @@ -335,3 +338,34 @@ it("separately rejects a tampered digest before admission to any consumer or ses }); } }); + +it("keeps withheld ownership unknown through CLI and MCP comparison", async () => { + const { client, right } = await connectedComparison(); + const left = captureEvidence("left", { + ...EMPTY_PROCESS_CAPTURE_EXAMPLE, + cleanup: { + ...EMPTY_PROCESS_CAPTURE_EXAMPLE.cleanup, + unverified_processes: [ + { pid: 900, reason: "platform_binary_environment_withheld" }, + ], + }, + residual_unknowns: [ + { scope: "process", reason: "Ownership of process 900 was withheld" }, + ], + }); + const response = await client.callTool({ + name: "compare_process_captures", + arguments: { left, right }, + }); + expect(response.isError, JSON.stringify(response)).not.toBe(true); + const comparison = inlineEvidence(response); + expect(comparison.normalized_result).toMatchObject({ status: "unknown" }); + const root = await createTestTempDirectory("rea-withheld-ownership-"); + const leftPath = join(root, "left.json"); + const rightPath = join(root, "right.json"); + await writeFile(leftPath, JSON.stringify(left)); + await writeFile(rightPath, JSON.stringify(right)); + expect( + parseEvidence(await compareProcessEvidenceFiles(leftPath, rightPath)), + ).toEqual(comparison); +}); diff --git a/tests/boundary/mcp/processTraceAdaptersParity.test.ts b/tests/boundary/mcp/processTraceAdaptersParity.test.ts index 64b45a220a..d27f37e844 100644 --- a/tests/boundary/mcp/processTraceAdaptersParity.test.ts +++ b/tests/boundary/mcp/processTraceAdaptersParity.test.ts @@ -6,12 +6,12 @@ import { afterEach, describe, expect, it } from "vitest"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import type { AnalysisClient } from "../../../src/application/AnalysisProvider.js"; -import { compareProcessEvidenceFiles } from "../../../src/application/ProcessCli.js"; -import { PROCESS_PROVIDER } from "../../../src/application/ProcessEvidence.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/processCaptureExample.js"; +import { compareProcessEvidenceFiles } from "../../../src/application/process/ProcessCli.js"; +import { PROCESS_PROVIDER } from "../../../src/application/process/ProcessEvidence.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/process/processCaptureExample.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; -import type { ProcessTraceSpecification } from "../../../src/domain/processTraceComparison.js"; +import type { ProcessTraceSpecification } from "../../../src/domain/process/processTraceComparison.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/promptCompletion.test.ts b/tests/boundary/mcp/promptCompletion.test.ts index f1807bc992..8b8b31dbbc 100644 --- a/tests/boundary/mcp/promptCompletion.test.ts +++ b/tests/boundary/mcp/promptCompletion.test.ts @@ -8,6 +8,7 @@ import type { AnalysisClient } from "../../../src/application/AnalysisProvider.j import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js"; import { PROCESS_CAPTURE_REFERENCE } from "../../../src/contracts/investigationExamples.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { createArtifactInspection } from "../../../src/domain/artifactInspection.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; @@ -57,6 +58,36 @@ describe("guided prompt completion from live analysis", () => { }); describe("guided prompt completion from investigation records", () => { + it("completes process captures imported in the original v3 format", async () => { + const session = createTestBinarySession(() => client([])); + const { + selected_executable_sha256: _selectedDigest, + executable_identity: _identity, + executable_sha256, + ...legacyManifest + } = EMPTY_PROCESS_CAPTURE_EXAMPLE.manifest; + const legacyCapture = { + ...EMPTY_PROCESS_CAPTURE_EXAMPLE, + manifest: { ...legacyManifest, executable_sha256 }, + }; + const evidence = createEvidence(undefined, fixtureProvider, { + operation: "capture_process_scenario", + parameters: {}, + result: legacyCapture, + confidence: "observed", + authority: "controlled-replay", + environment: fixtureEnvironment, + }); + expect(session.recordEvidence(evidence).ok).toBe(true); + const completion = createPromptCompletionSource(session, session); + + expect( + await completion.complete("capture", evidence.evidence_id.slice(0, 8)), + ).toEqual([evidence.evidence_id]); + + await session.close(); + }); + it("projects only validated typed identifiers from the evidence ledger", async () => { const session = createTestBinarySession(() => client([])); const invalidCapture = createEvidence(undefined, fixtureProvider, { diff --git a/tests/boundary/mcp/recordedCrashMcp.test.ts b/tests/boundary/mcp/recordedCrashMcp.test.ts new file mode 100644 index 0000000000..364c280d16 --- /dev/null +++ b/tests/boundary/mcp/recordedCrashMcp.test.ts @@ -0,0 +1,76 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it, onTestFinished } from "vitest"; +import { RecordedCrashService } from "../../../src/application/binaryDiagnostics/RecordedCrashService.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + recordedCrashFixture, + RECORDED_CRASH_TEST_PROVIDER, +} from "../../fixtures/binaryDiagnostics/recordedCrash.js"; + +it("advertises exact valid schemas and records inline recorded-core evidence without a binary target", async () => { + const value = recordedCrashFixture(); + const service = new RecordedCrashService({ + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(value)), + }); + const session = createTestBinarySession(() => { + throw new Error("deep provider must not start"); + }); + const server = createServer(session, session, { recordedCrash: service }); + const client = new Client({ name: "recorded-crash-contract", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = (await client.listTools()).tools.find( + (tool) => tool.name === "inspect_recorded_crash", + ); + if (advertised?.outputSchema === undefined) + throw new Error("recorded crash must publish both schemas"); + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + const inputSchema: Record = advertised.inputSchema; + const outputSchema: Record = advertised.outputSchema; + expect(ajv.validateSchema(inputSchema)).toBe(true); + expect(ajv.validateSchema(outputSchema)).toBe(true); + expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true); + expect( + ajv.validate(inputSchema, { + path: value.artifact.path, + approval: true, + }), + ).toBe(false); + const response = await client.callTool({ + name: "inspect_recorded_crash", + arguments: { path: value.artifact.path }, + }); + expect(response.isError).not.toBe(true); + expect( + ajv.validate(outputSchema, response.structuredContent), + JSON.stringify(ajv.errors), + ).toBe(true); + const parsed = toolContract("inspect_recorded_crash").outputSchema.parse( + response.structuredContent, + ); + const evidence = parseEvidence(parsed.evidence); + expect(parsed.result).toEqual(value); + expect(parsed.result).toEqual(evidence.normalized_result); + expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence); + expect( + ( + await client.callTool({ + name: "inspect_recorded_crash", + arguments: { path: "relative.elf" }, + }) + ).isError, + ).toBe(true); +}); diff --git a/tests/boundary/mcp/serverUnknownsMcp.test.ts b/tests/boundary/mcp/serverUnknownsMcp.test.ts index 57000324cd..c08aedfd13 100644 --- a/tests/boundary/mcp/serverUnknownsMcp.test.ts +++ b/tests/boundary/mcp/serverUnknownsMcp.test.ts @@ -15,8 +15,8 @@ import { observed as ok } from "../../fixtures/analysisExecution.js"; import { createServer } from "../../../src/server/createServer.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; -import { processCaptureSchema } from "../../../src/domain/processCapture.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/processCaptureExample.js"; +import { processCaptureSchema } from "../../../src/domain/process/processCapture.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/process/processCaptureExample.js"; import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; import { PROCESS_PROVIDER } from "../../../src/server/sessionToolPolicies.js"; diff --git a/tests/boundary/mcp/sessionMcp.test.ts b/tests/boundary/mcp/sessionMcp.test.ts index 637f00a8fb..cb3b0cd4f9 100644 --- a/tests/boundary/mcp/sessionMcp.test.ts +++ b/tests/boundary/mcp/sessionMcp.test.ts @@ -3,6 +3,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import type { CallToolResult } from "@modelcontextprotocol/server"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { afterEach, describe, expect, it } from "vitest"; import { z } from "zod"; @@ -14,11 +15,14 @@ import type { AnalysisProvider, CapabilityDescriptor, } from "../../../src/application/AnalysisProvider.js"; -import { probeProcessCaptureCapability } from "../../../src/application/ProcessHarness.js"; +import { probeProcessCaptureCapability } from "../../../src/process/capture/ProcessHarness.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; import { createServer } from "../../../src/server/createServer.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; import { silentLogger } from "../../../src/logger.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; +import { MAX_JSON_DEPTH } from "../../../src/domain/jsonValue.js"; +import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js"; import { ok as resultOk } from "../../../src/domain/result.js"; import { createSessionMcpHarness, @@ -227,6 +231,71 @@ describe("target-free MCP workflow", () => { }, 10_000); }); +describe("session filesystem path boundaries over MCP", () => { + it("rejects relative snapshot and export paths with an absolute-path error", async () => { + directory = await createTestTempDirectory("rea-mcp-path-boundary-"); + const { mcp, first } = await createSessionMcpHarness( + directory, + provider, + resources, + ); + + const exported = await mcp.callTool({ + name: "export_evidence_bundle", + arguments: { path: "relative-bundle.json" }, + }); + expect(exported.isError, JSON.stringify(exported.content)).toBe(true); + expect(JSON.stringify(exported.content)).toContain("absolute"); + + const opened = await mcp.callTool({ + name: "open_binary", + arguments: { path: first }, + }); + expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true); + const closed = await mcp.callTool({ + name: "close_binary", + arguments: { snapshot_path: "relative-analysis.json" }, + }); + expect(closed.isError, JSON.stringify(closed.content)).toBe(true); + expect(JSON.stringify(closed.content)).toContain("absolute"); + await mcp.callTool({ name: "close_binary", arguments: {} }); + }, 10_000); +}); + +describe("json depth bounds over MCP", () => { + it("classifies deeply nested evidence parameters as an input validation error", async () => { + directory = await createTestTempDirectory("rea-mcp-depth-bound-"); + const { mcp } = await createSessionMcpHarness( + directory, + provider, + resources, + ); + const input = INVESTIGATION_EXAMPLES.compare_process_captures.input; + const baseline = await mcp.callTool({ + name: "compare_process_captures", + arguments: input, + }); + expect(baseline.isError, JSON.stringify(baseline)).not.toBe(true); + let value: unknown = 1; + for (let index = 0; index <= MAX_JSON_DEPTH; index += 1) + value = { nested: value }; + const result = await mcp.callTool({ + name: "compare_process_captures", + arguments: { + left: { + ...input.left, + parameters: { attack: value }, + }, + right: input.right, + }, + }); + const text = JSON.stringify(result.content); + expect(result.isError, text).toBe(true); + expect(text).toContain("Input validation"); + expect(text).toContain("maximum nesting depth"); + }, 10_000); +}); + describe("process residuals over MCP", () => { it("records process residuals linked to capture Evidence", async () => { if (!(await probeProcessCaptureCapability()).available) return; @@ -249,6 +318,21 @@ describe("process residuals over MCP", () => { }, }); expect(captured.isError, text(captured)).not.toBe(true); + const contract = toolContract("capture_process_scenario"); + const wire = (await mcp.listTools()).tools.find( + ({ name }) => name === contract.name, + ); + if (wire?.outputSchema === undefined) + throw new Error("Missing process capture output schema"); + expect( + new Ajv2020({ strict: false, validateFormats: false }).validate( + z.record(z.string(), z.unknown()).parse(wire.outputSchema), + captured.structuredContent, + ), + ).toBe(true); + expect( + contract.outputSchema.safeParse(captured.structuredContent).success, + ).toBe(true); const listedUnknowns = await mcp.callTool({ name: "list_unknowns", arguments: {}, diff --git a/tests/boundary/mcp/toolRegistrationOptions.test.ts b/tests/boundary/mcp/toolRegistrationOptions.test.ts deleted file mode 100644 index af600d2e89..0000000000 --- a/tests/boundary/mcp/toolRegistrationOptions.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; -import { McpServer } from "@modelcontextprotocol/server"; - -import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; -import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js"; -import { annotationsFromEffects } from "../../../src/contracts/toolEffects.js"; -import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; - -describe("tool registration options", () => { - it("generates the checked-in catalog from the SDK wire projection", async () => { - const server = new McpServer({ name: "catalog-test", version: "0" }); - for (const contract of TOOL_CONTRACTS) - server.registerTool( - contract.name, - toolRegistrationOptions(contract), - async () => ({ - content: [{ type: "text" as const, text: "catalog-only" }], - isError: true as const, - }), - ); - const client = new Client({ name: "catalog-test", version: "0" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - try { - await server.connect(serverTransport); - await client.connect(clientTransport); - const advertised = (await client.listTools()).tools.map((tool) => ({ - name: tool.name, - title: tool.title, - description: tool.description, - inputSchema: tool.inputSchema, - outputSchema: tool.outputSchema, - annotations: tool.annotations, - })); - for (const contract of TOOL_CONTRACTS) { - const tool = advertised.find(({ name }) => name === contract.name); - expect(tool, contract.name).toBeDefined(); - expect(describesObject(tool?.inputSchema), contract.name).toBe(true); - expect(describesObject(tool?.outputSchema), contract.name).toBe(true); - expect(tool?.title?.trim().length, contract.name).toBeGreaterThan(0); - expect(tool?.description?.trim().length, contract.name).toBeGreaterThan( - 0, - ); - expect(JSON.stringify(tool?.outputSchema), contract.name).not.toContain( - '"result":{}', - ); - expect(tool?.annotations, contract.name).toEqual( - annotationsFromEffects(contract.effects), - ); - expect(contract.examples.length, contract.name).toBeGreaterThan(0); - for (const example of contract.examples) { - expect(example.title.trim().length, contract.name).toBeGreaterThan(0); - expect( - contract.inputSchema.safeParse(example.input).success, - `${contract.name}: ${example.title}`, - ).toBe(true); - } - - expect(tool?.inputSchema.examples, contract.name).toEqual( - contract.examples.map(({ input }) => input), - ); - expect( - missingPropertyDescriptions(tool?.inputSchema), - contract.name, - ).toEqual([]); - } - expect(advertised).toEqual( - GENERATED_MCP_TOOL_CATALOG.map((tool) => ({ - name: tool.name, - title: tool.title, - description: tool.description, - inputSchema: tool.inputSchema, - outputSchema: tool.outputSchema, - annotations: tool.annotations, - })), - ); - for (const [name, expected] of [ - ["binary_session", { readOnlyHint: true, openWorldHint: false }], - ["read_bytes", { readOnlyHint: false, destructiveHint: false }], - ["set_comment", { readOnlyHint: false }], - ["unset_bookmark", { destructiveHint: true }], - [ - "capture_process_scenario", - { readOnlyHint: false, openWorldHint: true }, - ], - ["inspect_artifact", { readOnlyHint: false, openWorldHint: true }], - ["extract_artifact", { readOnlyHint: false, idempotentHint: false }], - ["compare_web_captures", { readOnlyHint: false, openWorldHint: false }], - [ - "compare_web_screenshots", - { readOnlyHint: false, openWorldHint: false }, - ], - ] as const) { - expect( - advertised.find((tool) => tool.name === name)?.annotations, - name, - ).toMatchObject(expected); - } - } finally { - await Promise.allSettled([client.close(), server.close()]); - } - }); -}); - -const missingPropertyDescriptions = ( - value: unknown, - path = "inputSchema", -): string[] => { - if (Array.isArray(value)) - return value.flatMap((child, index) => - missingPropertyDescriptions(child, `${path}[${index}]`), - ); - if (!isObject(value)) return []; - - const missing: string[] = []; - if (isObject(value.properties)) - for (const [property, schema] of Object.entries(value.properties)) { - const propertyPath = `${path}.properties.${property}`; - if (!isObject(schema) || typeof schema.description !== "string") - missing.push(propertyPath); - missing.push(...missingPropertyDescriptions(schema, propertyPath)); - } - for (const [key, child] of Object.entries(value)) - if ( - key !== "properties" && - key !== "examples" && - key !== "default" && - key !== "const" && - key !== "enum" - ) - missing.push(...missingPropertyDescriptions(child, `${path}.${key}`)); - return missing; -}; - -const isObject = (value: unknown): value is Readonly> => - typeof value === "object" && value !== null && !Array.isArray(value); - -const describesObject = (schema: unknown): boolean => { - if (!isObject(schema)) return false; - return ( - schema.type === "object" || - [schema.oneOf, schema.anyOf].some( - (variants) => - Array.isArray(variants) && - variants.length > 0 && - variants.every( - (variant: unknown) => isObject(variant) && variant.type === "object", - ), - ) - ); -}; diff --git a/tests/boundary/mcp/toolResult.test.ts b/tests/boundary/mcp/toolResult.test.ts index 224f47380d..eb00c78743 100644 --- a/tests/boundary/mcp/toolResult.test.ts +++ b/tests/boundary/mcp/toolResult.test.ts @@ -1,14 +1,24 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; -import type { ToolContract } from "../../../src/contracts/toolContracts.js"; -import { ok } from "../../../src/domain/result.js"; -import { err } from "../../../src/domain/result.js"; +import { ProcessCaptureError } from "../../../src/process/capture/ProcessCaptureError.js"; +import { resolveProcessResult } from "../../../src/process/capture/ProcessCaptureLifecycle.js"; +import { + toolContract, + type ToolContract, +} from "../../../src/contracts/toolContracts.js"; +import { err, ok } from "../../../src/domain/result.js"; import { HopperProcessError } from "../../../src/domain/hopperErrors.js"; +import type { IncompleteProcessCaptureObservations } from "../../../src/domain/process/processCapture.js"; +import { processScenarioSchema } from "../../../src/domain/process/processScenario.js"; import { toCallToolResult } from "../../../src/server/toolResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { emptyProcessCapture } from "../../../src/domain/process/processCapture.fixture.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; import { evidenceResultOf } from "../../../src/contracts/toolOutputSchemas.js"; +import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; const contract: ToolContract = { name: "provider_neutral_fixture", @@ -36,6 +46,215 @@ const contract: ToolContract = { examples: [{ title: "Example fixture request", input: {} }], }; +describe("completed partial process capture MCP projection", () => { + it("preserves partial capture details over the MCP SDK transport", async () => { + const executionFailure = new Error("capture ended after a fixture error"); + const cleanup = { + owned_process_group: { + state: "unverified" as const, + reason: "process ownership token could not be read", + }, + terminal_renderer: { state: "cleaned" as const, reason: null }, + temporary_root: { state: "cleaned" as const, reason: null }, + }; + const capture = { + ...emptyProcessCapture(), + frames: [{ sequence: 0, at_ms: 0, data: "observed output" }], + event_journal: [], + }; + + let failure: ProcessCaptureError | undefined; + try { + resolveProcessResult(capture, executionFailure, cleanup); + } catch (cause: unknown) { + if (!(cause instanceof ProcessCaptureError)) throw cause; + failure = cause; + } + expect(failure).toBeDefined(); + if (failure === undefined) + throw new Error("expected cleanup-incomplete result"); + + const captureContract = toolContract("capture_process_scenario"); + const server = new McpServer({ + name: "process-partial-error", + version: "1", + }); + server.registerTool( + captureContract.name, + toolRegistrationOptions(captureContract), + async () => toCallToolResult(err(failure), captureContract), + ); + const client = new Client({ + name: "process-partial-error-client", + version: "1", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const result = await client.callTool({ + name: captureContract.name, + arguments: captureContract.examples[0]?.input ?? {}, + }); + + expect(result.isError).toBe(true); + expect(result.structuredContent).toMatchObject({ + error: { + code: "cleanup_incomplete", + details: { + cleanup_report: cleanup, + execution_failure: "capture ended after a fixture error", + partial_observation: { + capture: { + frames: [{ data: "observed output" }], + settlement: { cleanup_outcome: "failed" }, + }, + execution_failure: "capture ended after a fixture error", + }, + }, + }, + }); + const textContent = result.content.find((block) => block.type === "text"); + expect(textContent?.type === "text" ? textContent.text : undefined).toBe( + JSON.stringify(result.structuredContent), + ); + } finally { + await Promise.all([client.close(), server.close()]); + } + }); +}); + +describe("incomplete partial process observations MCP projection", () => { + it("preserves normalized raw observations over the MCP SDK transport", async () => { + const captureContract = toolContract("capture_process_scenario"); + const example = captureContract.examples[0]; + if (example === undefined) + throw new Error("missing process scenario example"); + const scenario = processScenarioSchema.parse({ + ...example.input, + normalization: { pids: false }, + }); + const rootPid = 45678; + const cleanup = { + owned_process_group: { + state: "unverified" as const, + reason: "process ownership token could not be read", + }, + terminal_renderer: { state: "cleaned" as const, reason: null }, + temporary_root: { state: "cleaned" as const, reason: null }, + }; + const observations: IncompleteProcessCaptureObservations = { + target_pid: { state: "available", value: rootPid }, + frames: { + state: "available", + value: [{ sequence: 0, at_ms: 0, data: "partial output" }], + }, + rendered_frames: { + state: "unavailable", + reason: "fixture renderer observation unavailable", + }, + interaction_events: { state: "available", value: [] }, + exit: { + state: "unavailable", + reason: "fixture terminal exit unavailable", + }, + settlement: { + state: "unavailable", + reason: "fixture process settlement unavailable", + }, + process_samples: { + state: "available", + value: [ + { + at_ms: 1, + pid: rootPid, + parent_pid: 1, + command: `fixture ${String(rootPid)}`, + process_group_id: rootPid, + session_id: rootPid, + }, + ], + }, + filesystem_snapshots: { + before: { state: "available", value: { files: [], truncated: false } }, + after: { + state: "unavailable", + reason: "fixture final snapshot unavailable", + }, + }, + event_journal: { state: "available", value: [] }, + manifest: { + state: "unavailable", + reason: "fixture capture manifest unavailable", + }, + }; + + let failure: ProcessCaptureError | undefined; + try { + resolveProcessResult( + undefined, + new Error("capture failed before completion"), + cleanup, + observations, + { scenario, rootPid }, + ); + } catch (cause: unknown) { + if (!(cause instanceof ProcessCaptureError)) throw cause; + failure = cause; + } + if (failure === undefined) + throw new Error("expected cleanup-incomplete result"); + + const server = new McpServer({ + name: "process-partial-observations", + version: "1", + }); + server.registerTool( + captureContract.name, + toolRegistrationOptions(captureContract), + async () => toCallToolResult(err(failure), captureContract), + ); + const client = new Client({ + name: "process-partial-observations-client", + version: "1", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const result = await client.callTool({ + name: captureContract.name, + arguments: example.input, + }); + + expect(result.isError).toBe(true); + expect(result.structuredContent).toMatchObject({ + error: { + details: { + partial_observation: { + observations: { + target_pid: { state: "available", value: rootPid }, + frames: { + state: "available", + value: [{ data: "partial output" }], + }, + process_samples: { + state: "available", + value: [{ pid: rootPid }], + }, + }, + }, + }, + }, + }); + } finally { + await Promise.all([client.close(), server.close()]); + } + }); +}); + describe("tool result projection", () => { it("exposes an actionable adapter code to MCP callers", () => { const result = toCallToolResult(err(new HopperProcessError(76)), contract); diff --git a/tests/boundary/mcp/toolSchemaValidity.test.ts b/tests/boundary/mcp/toolSchemaValidity.test.ts index e7ad269e0d..d2ac26ddfe 100644 --- a/tests/boundary/mcp/toolSchemaValidity.test.ts +++ b/tests/boundary/mcp/toolSchemaValidity.test.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js"; -import { processScenarioSchema } from "../../../src/domain/processScenario.js"; +import { processScenarioSchema } from "../../../src/domain/process/processScenario.js"; import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js"; import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; @@ -14,6 +14,7 @@ interface ToolSchemas { readonly name: string; readonly inputSchema: Record; readonly outputSchema?: Record | undefined; + readonly annotations?: Record | undefined; } function schemaErrors(tools: readonly ToolSchemas[]): string[] { @@ -28,6 +29,155 @@ function schemaErrors(tools: readonly ToolSchemas[]): string[] { ); } +function expectStrictInputSchemaParity( + tools: readonly ToolSchemas[], + ajv: Ajv2020, +): void { + const advertised = new Map(tools.map((tool) => [tool.name, tool])); + const names = [ + "inspect_managed_artifact", + "inspect_managed_members", + "inspect_managed_native_boundaries", + "list_browser_targets", + "open_binary", + "close_binary", + "binary_session", + "find_changed_behavior", + "build_call_path", + "record_unknown", + "update_unknown", + ]; + for (const name of names) { + const contract = TOOL_CONTRACTS.find( + ({ name: toolName }) => toolName === name, + ); + const tool = advertised.get(name); + const example = contract?.examples[0]; + if (contract === undefined || tool === undefined || example === undefined) + throw new Error(`${name} did not have an advertised example`); + const malformed = { ...example.input, __unexpected_root_key__: true }; + expect(contract.inputSchema.safeParse(malformed).success, name).toBe(false); + expect(ajv.compile(tool.inputSchema)(malformed), name).toBe(false); + } +} + +function expectKnownAuthorityHints(tools: readonly ToolSchemas[]): void { + const advertised = new Map(tools.map((tool) => [tool.name, tool])); + const expected = { + read_bytes: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + annotate_native_function: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + unset_bookmark: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + }, + open_binary: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + close_binary: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: false, + }, + } as const; + + for (const [name, annotations] of Object.entries(expected)) + expect(advertised.get(name), name).toMatchObject({ annotations }); +} + +function expectRecursivePropertyDescriptions( + schema: unknown, + path: string, +): void { + if (!isRecord(schema)) return; + if (isRecord(schema.properties)) { + for (const [property, child] of Object.entries(schema.properties)) { + expect(child, `${path}.${property}`).toMatchObject({ + description: expect.any(String), + }); + expectRecursivePropertyDescriptions(child, `${path}.${property}`); + } + } + + for (const key of ["items", "additionalProperties"]) + if (schema[key] !== undefined) + expectRecursivePropertyDescriptions(schema[key], path); + for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) { + const children = schema[key]; + if (Array.isArray(children)) + children.forEach((child: unknown, index: number) => + expectRecursivePropertyDescriptions(child, `${path}.${key}[${index}]`), + ); + } +} + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +function resolveReference(schema: unknown, reference: string): unknown { + if (reference === "#") return schema; + if (!reference.startsWith("#/")) return undefined; + let value = schema; + for (const token of reference.slice(2).split("/")) { + const key = token.replaceAll("~1", "/").replaceAll("~0", "~"); + if ( + typeof value !== "object" || + value === null || + !Object.hasOwn(value, key) + ) + return undefined; + value = Reflect.get(value, key); + } + return value; +} + +function recursiveReferences(tools: readonly ToolSchemas[]): string[] { + return tools.flatMap((tool) => + (["inputSchema", "outputSchema"] as const).flatMap((kind) => { + const schema = tool[kind]; + const found = new Set(); + const visit = (node: unknown, active: readonly string[]): void => { + if (typeof node !== "object" || node === null) return; + for (const [key, child] of Object.entries(node)) { + if ( + [ + "$defs", + "definitions", + "examples", + "default", + "const", + "enum", + ].includes(key) + ) + continue; + if (key !== "$ref" || typeof child !== "string") visit(child, active); + else if (active.includes(child)) found.add(child); + else visit(resolveReference(schema, child), [...active, child]); + } + }; + visit(schema, []); + return [...found].map( + (reference) => `${tool.name}.${kind}: ${reference}`, + ); + }), + ); +} + const advertiseAndEnforceProcessEnvironmentKeyConstraint = async (): Promise => { const contract = TOOL_CONTRACTS.find( @@ -118,15 +268,6 @@ describe("MCP JSON Schema validity", () => { expect(validate(value)).toBe(false); }); - it("uses an oracle that rejects empty prefixItems under Draft 2020-12", () => { - const ajv = new Ajv2020({ strict: false, validateFormats: false }); - expect(ajv.defaultMeta()).toBe( - "https://json-schema.org/draft/2020-12/schema", - ); - expect(ajv.validateSchema({ type: "array", prefixItems: [] })).toBe(false); - expect(ajv.validateSchema({ type: "array", maxItems: 0 })).toBe(true); - }); - it("advertises valid input and output schemas for every canonical tool", async () => { const server = new McpServer({ name: "schema-validation", version: "0" }); const client = new Client({ name: "schema-validation", version: "0" }); @@ -148,12 +289,49 @@ describe("MCP JSON Schema validity", () => { TOOL_CONTRACTS.map(({ name }) => name).sort(), ); expect(schemaErrors(tools)).toEqual([]); + expect(recursiveReferences(tools)).toEqual([]); + expectKnownAuthorityHints(tools); + const byName = new Map(tools.map((tool) => [tool.name, tool])); + const catalogProjection = tools.map((tool) => ({ + name: tool.name, + title: tool.title, + description: tool.description, + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + annotations: tool.annotations, + })); + expect(catalogProjection).toEqual( + GENERATED_MCP_TOOL_CATALOG.map((tool) => ({ + name: tool.name, + title: tool.title, + description: tool.description, + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + annotations: tool.annotations, + })), + ); + for (const contract of TOOL_CONTRACTS) { + const tool = byName.get(contract.name); + expect(tool?.title?.trim(), contract.name).toBeTruthy(); + expect(tool?.description?.trim(), contract.name).toBeTruthy(); + expect(tool?.inputSchema.examples, contract.name).toEqual( + contract.examples.map(({ input }) => input), + ); + for (const example of contract.examples) + expect( + contract.inputSchema.safeParse(example.input).success, + `${contract.name}: ${example.title}`, + ).toBe(true); + expectRecursivePropertyDescriptions(tool?.inputSchema, contract.name); + } } finally { await Promise.allSettled([client.close(), server.close()]); } }); +}); - it("advertises the structural request requirements enforced at runtime", async () => { +describe("MCP root input schemas", () => { + it("advertises object roots while enforcing union requirements at runtime", async () => { const server = new McpServer({ name: "schema-constraints", version: "0" }); const client = new Client({ name: "schema-constraints", version: "0" }); const [clientTransport, serverTransport] = @@ -177,12 +355,18 @@ describe("MCP JSON Schema validity", () => { if (graphContract === undefined || graphTool === undefined) throw new Error("Managed application graph tool was not advertised"); expect(graphContract.inputSchema.safeParse({}).success).toBe(false); + expect(graphTool.inputSchema).toMatchObject({ type: "object" }); expect(ajv.compile(graphTool.inputSchema)({})).toBe(false); + expect(ajv.compile(graphTool.inputSchema)({ unrelated: true })).toBe( + false, + ); for (const contract of TOOL_CONTRACTS) { - const validate = ajv.compile( - advertised.get(contract.name)!.inputSchema, - ); + const inputSchema = advertised.get(contract.name)!.inputSchema; + expect(inputSchema.type, contract.name).toBe("object"); + expect(inputSchema.properties, contract.name).toBeDefined(); + expect(inputSchema.anyOf, contract.name).toBeUndefined(); + const validate = ajv.compile(inputSchema); for (const example of contract.examples) expect( validate(example.input), @@ -190,6 +374,8 @@ describe("MCP JSON Schema validity", () => { ).toBe(true); } + expectStrictInputSchemaParity(tools, ajv); + const nativeObservation = TOOL_CONTRACTS.find( ({ name }) => name === "observe_native_ui", ); @@ -220,13 +406,98 @@ describe("MCP JSON Schema validity", () => { await Promise.allSettled([client.close(), server.close()]); } }); - - it("ships valid input and output schemas in the generated catalog", () => { - expect(schemaErrors(GENERATED_MCP_TOOL_CATALOG)).toEqual([]); - }); }); it( "advertises and enforces the process-owned environment key constraint", advertiseAndEnforceProcessEnvironmentKeyConstraint, ); + +describe("MCP process input JSON Schema", () => { + it("advertises process strings without weakening terminal input", async () => { + const server = new McpServer({ + name: "process-schema-validation", + version: "0", + }); + const client = new Client({ + name: "process-schema-validation", + version: "0", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + for (const contract of TOOL_CONTRACTS) + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + async () => ({ content: [] }), + ); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const processContract = TOOL_CONTRACTS.find( + ({ name }) => name === "capture_process_scenario", + ); + const processTool = (await client.listTools()).tools.find( + ({ name }) => name === "capture_process_scenario", + ); + if (processContract === undefined || processTool === undefined) + throw new Error("Process scenario tool was not advertised"); + const validate = new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(processTool.inputSchema); + const base = { executable: "/usr/bin/true" }; + for (const input of [ + { ...base, executable: "/usr/bin/true\0" }, + { ...base, arguments: ["\0"] }, + { ...base, working_directory: "/tmp\0" }, + { ...base, environment: { KEY: "value\0" } }, + { ...base, filesystem_observation_paths: ["/tmp\0"] }, + ]) { + expect(processContract.inputSchema.safeParse(input).success).toBe( + false, + ); + expect(validate(input)).toBe(false); + } + const terminalInput = { + ...base, + events: [{ type: "input", at_ms: 0, data: "\0" }], + }; + expect(processContract.inputSchema.safeParse(terminalInput).success).toBe( + true, + ); + expect(validate(terminalInput)).toBe(true); + } finally { + await Promise.allSettled([client.close(), server.close()]); + } + }); +}); + +it("ships nonrecursive input and output schemas in the generated catalog", () => { + expect(schemaErrors(GENERATED_MCP_TOOL_CATALOG)).toEqual([]); + expect(recursiveReferences(GENERATED_MCP_TOOL_CATALOG)).toEqual([]); +}); + +it("distinguishes recursive references from shared definitions", () => { + const diamond = { + type: "object", + properties: { + left: { $ref: "#/$defs/shared" }, + right: { $ref: "#/$defs/shared" }, + }, + $defs: { shared: { type: "string" } }, + }; + expect( + recursiveReferences([{ name: "diamond", inputSchema: diamond }]), + ).toEqual([]); + expect( + recursiveReferences([{ name: "root", inputSchema: { $ref: "#" } }]), + ).toEqual(["root.inputSchema: #"]); + const escaped = { + $ref: "#/$defs/a~1b", + $defs: { "a/b": { $ref: "#/$defs/a~1b" } }, + }; + expect( + recursiveReferences([{ name: "escaped", inputSchema: escaped }]), + ).toEqual(["escaped.inputSchema: #/$defs/a~1b"]); +}); diff --git a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts new file mode 100644 index 0000000000..fdbe0753e9 --- /dev/null +++ b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts @@ -0,0 +1,101 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it, onTestFinished } from "vitest"; +import { WebNetworkCaptureService } from "../../../src/application/WebNetworkCaptureService.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { ok } from "../../../src/domain/result.js"; +import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; + +it("publishes valid SDK schemas and retains historical inline Evidence with the supplied session", async () => { + const service = new WebNetworkCaptureService({ + inspect: () => + Promise.resolve( + ok({ + artifact: { path: "/capture.har", sha256: "a".repeat(64), bytes: 10 }, + format: "har", + decoder: { id: "test-capture-port", name: "test port", version: "1" }, + container: { + reported: { creator: "fixture", ["__proto__"]: { preserved: 7 } }, + numeric_literals: [], + redactions: [], + records_pointer: "/log/entries", + }, + total_records: 0, + records: [], + runtime_attribution: "unknown", + limitations: ["synthetic application port, no real decoder claim"], + }), + ), + }); + const session = createTestBinarySession(() => { + throw new Error("Binary provider must not start"); + }); + const server = createServer(session, session, { webNetworkCapture: service }); + const client = new Client({ + name: "historical-capture-contract", + version: "1", + }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = (await client.listTools()).tools.find( + (tool) => tool.name === "inspect_web_network_capture", + ); + if (advertised?.outputSchema === undefined) + throw new Error("Capture inspection must advertise both schemas"); + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + const inputSchema: Record = advertised.inputSchema; + const outputSchema: Record = advertised.outputSchema; + expect(ajv.validateSchema(inputSchema)).toBe(true); + expect(ajv.validateSchema(outputSchema)).toBe(true); + const args = { capture_path: "/capture.har", format: "har" }; + expect(ajv.validate(inputSchema, args)).toBe(true); + const response = await client.callTool({ + name: "inspect_web_network_capture", + arguments: args, + }); + expect(response.isError).not.toBe(true); + expect( + ajv.validate(outputSchema, response.structuredContent), + JSON.stringify(ajv.errors), + ).toBe(true); + const parsed = toolContract("inspect_web_network_capture").outputSchema.parse( + response.structuredContent, + ); + const evidence = parseEvidence(parsed.evidence); + expect(parsed.result).toEqual(evidence.normalized_result); + const capture = webNetworkCaptureSchema.parse(parsed.result); + expect(capture.container.reported).toEqual({ + creator: "fixture", + ["__proto__"]: { preserved: 7 }, + }); + expect(Object.getPrototypeOf(capture.container.reported)).toBe( + Object.prototype, + ); + expect(Reflect.get(Object.prototype, "preserved")).toBeUndefined(); + expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence); + const invalid = await client.callTool({ + name: "inspect_web_network_capture", + arguments: { ...args, capture_path: "relative.har" }, + }); + expect(invalid.isError).toBe(true); + const invalidSensitive = await client.callTool({ + name: "inspect_web_network_capture", + arguments: { ...args, sensitive_values: ["secret"], mysecret: true }, + }); + expect(invalidSensitive.isError).toBe(true); + expect(invalidSensitive.structuredContent).toMatchObject({ + error: { code: "invalid_request", details: { issues: [{ path: [] }] } }, + }); + expect(JSON.stringify(invalidSensitive)).not.toContain("secret"); + expect(ajv.validate(inputSchema, { ...args, mysecret: true })).toBe(false); +}); diff --git a/tests/boundary/mcp/wireInputSchemaLimits.test.ts b/tests/boundary/mcp/wireInputSchemaLimits.test.ts new file mode 100644 index 0000000000..be8bdae1cb --- /dev/null +++ b/tests/boundary/mcp/wireInputSchemaLimits.test.ts @@ -0,0 +1,240 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; +import { browserCaptureComparisonInputSchema } from "../../../src/domain/browserCaptureComparison.js"; +import { webPageInspectionSchema } from "../../../src/domain/browserObservation.js"; +import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js"; +import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +function resolveLocalReference(root: unknown, reference: string): unknown { + if (reference === "#") return root; + if (!reference.startsWith("#/")) return undefined; + let current = root; + for (const part of reference.slice(2).split("/")) { + const key = part.replaceAll("~1", "/").replaceAll("~0", "~"); + if (!isRecord(current) || !Object.hasOwn(current, key)) return undefined; + current = current[key]; + } + return current; +} + +// Count object/array and composition levels, following references per path. +// Definitions, property maps, and literal example/default data are not levels. +function inputDepth( + schema: unknown, + root: unknown = schema, + activeReferences: readonly string[] = [], +): number { + if (!isRecord(schema)) return 0; + let referenceDepth = 0; + if (typeof schema.$ref === "string") { + if (activeReferences.includes(schema.$ref)) return Number.POSITIVE_INFINITY; + const target = resolveLocalReference(root, schema.$ref); + if (target === undefined) return Number.POSITIVE_INFINITY; + referenceDepth = inputDepth(target, root, [ + ...activeReferences, + schema.$ref, + ]); + } + const childDepths: number[] = []; + for (const key of ["properties", "patternProperties", "dependentSchemas"]) { + const group = schema[key]; + if (isRecord(group)) + for (const child of Object.values(group)) + childDepths.push(inputDepth(child, root, activeReferences)); + } + for (const key of [ + "items", + "additionalProperties", + "contains", + "not", + "if", + "then", + "else", + "propertyNames", + "unevaluatedProperties", + "unevaluatedItems", + ]) + childDepths.push(inputDepth(schema[key], root, activeReferences)); + for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) { + const children = schema[key]; + if (Array.isArray(children)) + for (const child of children) + childDepths.push(inputDepth(child, root, activeReferences)); + } + const types = Array.isArray(schema.type) ? schema.type : [schema.type]; + const container = types.includes("object") || types.includes("array") ? 1 : 0; + const composition = ["anyOf", "oneOf", "allOf"].some((key) => + Array.isArray(schema[key]), + ) + ? 1 + : 0; + return Math.max( + referenceDepth, + container + composition + Math.max(0, ...childDepths), + ); +} + +function inputDepthViolations( + tools: readonly { name: string; inputSchema: unknown }[], +) { + return tools.flatMap(({ name, inputSchema }) => { + const depth = inputDepth(inputSchema); + return depth > 10 ? [{ name, depth }] : []; + }); +} + +async function withCatalog( + verify: (client: Client, calls: ReadonlyMap) => Promise, +): Promise { + const server = new McpServer({ name: "wire-limits", version: "0" }); + const calls = new Map(); + for (const contract of TOOL_CONTRACTS) + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + async () => { + calls.set(contract.name, (calls.get(contract.name) ?? 0) + 1); + return { content: [], isError: true }; + }, + ); + const client = new Client({ name: "wire-limits", version: "0" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + await verify(client, calls); + } finally { + await Promise.allSettled([client.close(), server.close()]); + } +} + +describe("complete catalog input compatibility profile (#919)", () => { + it("follows shared and escaped references without treating data as schemas", () => { + const diamond = { + type: "object", + properties: { + left: { $ref: "#/$defs/a~1b~0c" }, + right: { $ref: "#/$defs/a~1b~0c" }, + }, + $defs: { + "a/b~c": { type: "object", properties: { id: { type: "string" } } }, + unused: { $ref: "#/$defs/unused" }, + }, + examples: [{ $ref: "#" }], + default: { $ref: "#" }, + }; + expect(inputDepth(diamond)).toBe(2); + expect( + inputDepth({ type: "object", properties: { child: { $ref: "#" } } }), + ).toBe(Number.POSITIVE_INFINITY); + expect( + inputDepth({ + type: "object", + properties: { + values: { + anyOf: [ + { type: "array", items: { type: "object" } }, + { type: "null" }, + ], + }, + }, + }), + ).toBe(4); + }); + + it("keeps every live and generated input within ten structural levels", async () => { + await withCatalog(async (client) => { + const { tools } = await client.listTools(); + expect(tools.map(({ name }) => name).sort()).toEqual( + TOOL_CONTRACTS.map(({ name }) => name).sort(), + ); + expect(inputDepthViolations(tools)).toEqual([]); + expect(inputDepthViolations(GENERATED_MCP_TOOL_CATALOG)).toEqual([]); + }); + }); + + it("describes capture round trips without altering domain or scenario schemas", () => { + const contract = TOOL_CONTRACTS.find( + ({ name }) => name === "compare_web_captures", + ); + if (contract === undefined) + throw new Error("Missing capture comparison contract"); + const original = z.toJSONSchema(browserCaptureComparisonInputSchema, { + io: "input", + }); + const projected = z.toJSONSchema(contract.inputSchema, { io: "input" }); + expect(inputDepth(original)).toBeGreaterThan(10); + expect(inputDepth(projected)).toBeLessThanOrEqual(10); + expect(isRecord(original.properties)).toBe(true); + expect(isRecord(projected.properties)).toBe(true); + if (!isRecord(original.properties) || !isRecord(projected.properties)) + throw new Error("Missing comparison properties"); + for (const key of ["normalization"]) + expect(projected.properties[key], key).toEqual(original.properties[key]); + expect(z.toJSONSchema(webPageInspectionSchema).properties).toHaveProperty( + "network", + ); + for (const example of contract.examples) + expect(contract.inputSchema.parse(example.input)).toEqual( + browserCaptureComparisonInputSchema.parse(example.input), + ); + const example = contract.examples[0]; + if (example === undefined) throw new Error("Missing scenario example"); + for (const malformed of [ + { before_scenario: {}, after_scenario: {} }, + { + ...example.input, + before: { inspection: {} }, + after: { inspection: {} }, + }, + { ...example.input, __unexpected_root_key__: true }, + ]) { + const originalResult = + browserCaptureComparisonInputSchema.safeParse(malformed); + const projectedResult = contract.inputSchema.safeParse(malformed); + expect(originalResult.success).toBe(false); + expect(projectedResult.success).toBe(false); + if (!originalResult.success && !projectedResult.success) + expect(projectedResult.error.issues).toEqual( + originalResult.error.issues, + ); + } + }); + + it("rejects incomplete round-trip payloads before invoking the handler", async () => { + await withCatalog(async (client, calls) => { + const tool = (await client.listTools()).tools.find( + ({ name }) => name === "compare_web_captures", + ); + if (tool === undefined) + throw new Error("Missing advertised comparison tool"); + const incomplete = { + before: { inspection: {} }, + after: { inspection: {} }, + }; + const validate = new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(tool.inputSchema); + expect(validate(incomplete)).toBe(true); + expect( + browserCaptureComparisonInputSchema.safeParse(incomplete).success, + ).toBe(false); + const result = await client.callTool({ + name: tool.name, + arguments: incomplete, + }); + expect(result.isError).toBe(true); + expect(calls.get(tool.name) ?? 0).toBe(0); + }); + }); +}); diff --git a/src/application/ProcessEvidence.host.test.ts b/tests/boundary/process/ProcessEvidence.host.test.ts similarity index 67% rename from src/application/ProcessEvidence.host.test.ts rename to tests/boundary/process/ProcessEvidence.host.test.ts index d241463620..27300d44ea 100644 --- a/src/application/ProcessEvidence.host.test.ts +++ b/tests/boundary/process/ProcessEvidence.host.test.ts @@ -1,10 +1,10 @@ import { expect, it } from "vitest"; -import { emptyUnverifiedProcessCapture } from "../domain/processCapture.fixture.js"; -import { parseProcessCapture } from "../domain/processCapture.js"; -import { parseProcessScenario } from "../domain/processScenario.js"; -import { createProcessCaptureEvidence } from "./ProcessEvidence.js"; -import { observeSettlement } from "./ProcessCaptureLifecycle.js"; +import { emptyUnverifiedProcessCapture } from "../../../src/domain/process/processCapture.fixture.js"; +import { parseProcessCapture } from "../../../src/domain/process/processCapture.js"; +import { parseProcessScenario } from "../../../src/domain/process/processScenario.js"; +import { createProcessCaptureEvidence } from "../../../src/application/process/ProcessEvidence.js"; +import { observeSettlement } from "../../../src/process/capture/ProcessCaptureLifecycle.js"; it("stamps process Evidence from the capture manifest host", () => { const scenario = parseProcessScenario({ diff --git a/src/application/ProcessEvidence.test.ts b/tests/boundary/process/ProcessEvidence.test.ts similarity index 84% rename from src/application/ProcessEvidence.test.ts rename to tests/boundary/process/ProcessEvidence.test.ts index b286cdcf07..1c35de7619 100644 --- a/src/application/ProcessEvidence.test.ts +++ b/tests/boundary/process/ProcessEvidence.test.ts @@ -1,18 +1,22 @@ import { expect, it } from "vitest"; -import { createProcessCaptureEvidence } from "./ProcessEvidence.js"; -import { createRunManifest } from "./ProcessCaptureLifecycle.js"; -import { emptyUnverifiedProcessCapture } from "../domain/processCapture.fixture.js"; +import { createProcessCaptureEvidence } from "../../../src/application/process/ProcessEvidence.js"; +import { + createRunManifest, + observeLaunchedExecutable, + observeSelectedExecutable, +} from "../../../src/process/capture/ProcessCaptureLifecycle.js"; +import { emptyUnverifiedProcessCapture } from "../../../src/domain/process/processCapture.fixture.js"; import { compareProcessCaptures, parseProcessCapture, -} from "../domain/processCapture.js"; +} from "../../../src/domain/process/processCapture.js"; import { digestProcessCommitment, parseProcessScenario, processComparisonContract, processScenarioCommitment, -} from "../domain/processScenario.js"; +} from "../../../src/domain/process/processScenario.js"; const captureEvidenceForSensitiveInput = (data: string) => { const scenario = parseProcessScenario({ @@ -61,8 +65,20 @@ const captureEvidenceForEnvironmentSecret = async (value: string) => { environment: { API_TOKEN: value }, events: [{ type: "input", at_ms: 0, data: value }], }); + const selectedExecutable = await observeSelectedExecutable( + scenario.executable, + ); + const executableIdentity = observeLaunchedExecutable( + scenario.executable, + selectedExecutable, + ); + const manifest = await createRunManifest( + scenario, + new Date(0), + new Date(1), + executableIdentity, + ); const base = emptyUnverifiedProcessCapture(); - const manifest = await createRunManifest(scenario, new Date(0), new Date(1)); const scenarioManifest = { ...manifest, normalization_sha256: digestProcessCommitment(scenario.normalization), diff --git a/tests/boundary/process/darwinProcessRunTokenReader.test.ts b/tests/boundary/process/darwinProcessRunTokenReader.test.ts new file mode 100644 index 0000000000..0c41c0ffd0 --- /dev/null +++ b/tests/boundary/process/darwinProcessRunTokenReader.test.ts @@ -0,0 +1,724 @@ +import { execFile, spawn, type ChildProcess } from "node:child_process"; +import { + access, + chmod, + mkdtemp, + readFile, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { _electron as electron } from "playwright-core"; +import { expect, it, vi } from "vitest"; + +import { + createSystemProcessOwnershipHost, + systemProcessOwnershipHost, +} from "../../../src/process/ProcessOwnershipObservation.js"; +import { createDarwinProcessRunTokenReader } from "../../../src/process/DarwinProcessRunTokenReader.js"; +import { execFileOutput } from "../../../src/process/ExecFileOutput.js"; +import { normalizeCaptureFailure } from "../../../src/process/capture/ProcessHarness.js"; +import { PlaywrightElectronActiveProvider } from "../../../src/browser/PlaywrightElectronActiveProvider.js"; +import { electronActiveObservationInputSchema } from "../../../src/domain/javascript/electronActiveObservation.js"; + +const execFileAsync = promisify(execFile); +const onDarwin = process.platform === "darwin"; + +const writeBlockingCompiler = async (directory: string) => { + const executable = join(directory, "fake-xcrun"); + const statePath = join(directory, "compile-state.json"); + const releasePath = join(directory, "release-compiler"); + const source = `#!/usr/bin/env node +const fs = require("node:fs"); +const path = require("node:path"); +const statePath = ${JSON.stringify(statePath)}; +const releasePath = ${JSON.stringify(releasePath)}; +const state = fs.existsSync(statePath) + ? JSON.parse(fs.readFileSync(statePath, "utf8")) + : { calls: 0 }; +state.calls += 1; +const moduleIndex = process.argv.indexOf("-module-cache-path"); +state.root = path.dirname(process.argv[moduleIndex + 1]); +const outputIndex = process.argv.indexOf("-o"); +state.output = process.argv[outputIndex + 1]; +state.releasePath = releasePath; +fs.writeFileSync(statePath, JSON.stringify(state)); +if (state.calls === 1) { + const timer = setInterval(() => { + if (!fs.existsSync(releasePath)) return; + clearInterval(timer); + fs.writeFileSync(state.output, "compiled"); + }, 10); +} else { + fs.writeFileSync(state.output, "compiled"); +} +`; + await writeFile(executable, source); + await chmod(executable, 0o755); + return { executable, statePath, releasePath }; +}; + +const writeBlockingIdentityCompiler = async (directory: string) => { + const executable = join(directory, "fake-identity-xcrun"); + const statePath = join(directory, "identity-helper-state.json"); + const helperSource = `#!/usr/bin/env node +const fs = require("node:fs"); +fs.writeFileSync(${JSON.stringify(statePath)}, JSON.stringify({ + root: __dirname, +})); +setInterval(() => {}, 1000); +`; + const compilerSource = `#!/usr/bin/env node +const fs = require("node:fs"); +const outputIndex = process.argv.indexOf("-o"); +const output = process.argv[outputIndex + 1]; +fs.writeFileSync(output, ${JSON.stringify(helperSource)}); +fs.chmodSync(output, 0o755); +`; + await writeFile(executable, compilerSource); + await chmod(executable, 0o755); + return { executable, statePath }; +}; + +const waitForCompileState = async (statePath: string) => { + const deadline = Date.now() + 5_000; + while (Date.now() < deadline) { + try { + const parsed: unknown = JSON.parse(await readFile(statePath, "utf8")); + if ( + typeof parsed === "object" && + parsed !== null && + "calls" in parsed && + typeof parsed.calls === "number" && + "root" in parsed && + typeof parsed.root === "string" && + "output" in parsed && + typeof parsed.output === "string" && + "releasePath" in parsed && + typeof parsed.releasePath === "string" + ) + return { + calls: parsed.calls, + root: parsed.root, + output: parsed.output, + releasePath: parsed.releasePath, + }; + } catch { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + throw new Error("fake compiler did not start"); +}; + +const waitForIdentityHelper = async (statePath: string) => { + const deadline = Date.now() + 5_000; + while (Date.now() < deadline) { + try { + const parsed: unknown = JSON.parse(await readFile(statePath, "utf8")); + if ( + typeof parsed === "object" && + parsed !== null && + "root" in parsed && + typeof parsed.root === "string" + ) + return { root: parsed.root }; + } catch { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + throw new Error("fake identity helper did not start"); +}; + +type ElectronCaptureResult = Awaited< + ReturnType +>; +type ElectronCaptureOutcome = + | { readonly state: "result"; readonly value: ElectronCaptureResult } + | { readonly state: "rejected"; readonly cause: unknown }; + +it("reports an actionable missing Swift compiler without installing it", async () => { + const reader = createDarwinProcessRunTokenReader({ + xcrun: join(tmpdir(), "rea-no-such-xcrun"), + }); + try { + await expect( + reader.read([ + { + pid: 1, + parentPid: 0, + processGroupId: 1, + state: "S", + command: "fixture", + }, + ]), + ).rejects.toThrow(/requires the Apple Swift compiler via xcrun/u); + } finally { + await reader.close(); + } +}); + +it.skipIf(process.platform === "win32")( + "rethrows compile cancellation, removes its temporary root, and permits retry", + async () => { + const directory = await mkdtemp( + join(tmpdir(), "rea-process-token-abort-test-"), + ); + const fakeCompiler = await writeBlockingCompiler(directory); + const reader = createDarwinProcessRunTokenReader({ + xcrun: fakeCompiler.executable, + }); + const controller = new AbortController(); + const compilation = reader.prepare(controller.signal).then( + () => ({ state: "fulfilled" as const }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + try { + const firstAttempt = await waitForCompileState(fakeCompiler.statePath); + controller.abort(); + const outcome = await compilation; + + expect(outcome.state).toBe("rejected"); + if (outcome.state !== "rejected") + throw new Error( + "aborted native reader compilation unexpectedly succeeded", + ); + expect(outcome.cause).toMatchObject({ name: "AbortError" }); + expect( + normalizeCaptureFailure(outcome.cause, controller.signal), + ).toMatchObject({ reason: "cancelled", userCategory: "cancelled" }); + await expect(access(firstAttempt.root)).rejects.toThrow(); + + await reader.prepare(); + const retry = await waitForCompileState(fakeCompiler.statePath); + expect(retry).toMatchObject({ calls: 2 }); + await expect(access(retry.output)).resolves.toBeUndefined(); + await reader.close(); + await expect(access(retry.root)).rejects.toThrow(); + } finally { + if (!controller.signal.aborted) controller.abort(); + await compilation; + await reader.close(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(process.platform === "win32").each(["first", "second"] as const)( + "keeps shared compilation alive when the %s waiter cancels", + async (cancelledWaiter) => { + const directory = await mkdtemp( + join(tmpdir(), "rea-process-token-shared-compile-test-"), + ); + const fakeCompiler = await writeBlockingCompiler(directory); + const reader = createDarwinProcessRunTokenReader({ + xcrun: fakeCompiler.executable, + }); + const firstController = new AbortController(); + const secondController = new AbortController(); + const firstWaiter = reader.prepare(firstController.signal).then( + (value) => ({ state: "fulfilled" as const, value }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + const secondWaiterPromise = (async () => { + const state = await waitForCompileState(fakeCompiler.statePath); + const secondWaiter = reader.prepare(secondController.signal).then( + (value) => ({ state: "fulfilled" as const, value }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + const cancelled = + cancelledWaiter === "first" ? firstWaiter : secondWaiter; + const surviving = + cancelledWaiter === "first" ? secondWaiter : firstWaiter; + (cancelledWaiter === "first" + ? firstController + : secondController + ).abort(); + const firstOutcome = await cancelled; + expect(firstOutcome.state).toBe("rejected"); + if (firstOutcome.state !== "rejected") + throw new Error("cancelled compilation waiter unexpectedly succeeded"); + expect(firstOutcome.cause).toMatchObject({ name: "AbortError" }); + await expect(access(state.root)).resolves.toBeUndefined(); + await writeFile(fakeCompiler.releasePath, "release"); + return { state, outcome: await surviving }; + })(); + + try { + const { state, outcome } = await secondWaiterPromise; + expect(state.calls).toBe(1); + expect(outcome.state).toBe("fulfilled"); + if (outcome.state !== "fulfilled") throw outcome.cause; + expect(outcome.value).toBe(state.output); + await reader.close(); + await expect(access(state.root)).rejects.toThrow(); + } finally { + if (!firstController.signal.aborted) firstController.abort(); + if (!secondController.signal.aborted) secondController.abort(); + await Promise.allSettled([firstWaiter, secondWaiterPromise]); + await reader.close(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( + "retains a failed compilation cleanup for a later close retry", + async () => { + const directory = await mkdtemp( + join(tmpdir(), "rea-process-token-cleanup-retry-test-"), + ); + const fakeCompiler = await writeBlockingCompiler(directory); + const reader = createDarwinProcessRunTokenReader({ + xcrun: fakeCompiler.executable, + }); + const controller = new AbortController(); + const preparation = reader.prepare(controller.signal).then( + () => ({ state: "fulfilled" as const }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + let compilationRoot: string | undefined; + try { + const state = await waitForCompileState(fakeCompiler.statePath); + compilationRoot = state.root; + await writeFile( + join(state.root, "retained.txt"), + "cleanup must remain tracked", + ); + await chmod(state.root, 0o000); + controller.abort(); + expect((await preparation).state).toBe("rejected"); + await expect(reader.close()).rejects.toThrow(); + await chmod(state.root, 0o700); + await reader.close(); + await expect(access(state.root)).rejects.toThrow(); + } finally { + if (!controller.signal.aborted) controller.abort(); + await preparation; + if (compilationRoot !== undefined) + await chmod(compilationRoot, 0o700).catch(() => undefined); + await reader.close(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(process.platform === "win32")( + "aborts and cleans an active compilation when the reader closes", + async () => { + const directory = await mkdtemp( + join(tmpdir(), "rea-process-token-close-compile-test-"), + ); + const fakeCompiler = await writeBlockingCompiler(directory); + const reader = createDarwinProcessRunTokenReader({ + xcrun: fakeCompiler.executable, + }); + const preparation = reader.prepare().then( + () => ({ state: "fulfilled" as const }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + try { + const state = await waitForCompileState(fakeCompiler.statePath); + await reader.close(); + const outcome = await preparation; + expect(outcome.state).toBe("rejected"); + if (outcome.state !== "rejected") + throw new Error("closed reader compilation unexpectedly succeeded"); + expect(outcome.cause).toMatchObject({ name: "AbortError" }); + await expect(access(state.root)).rejects.toThrow(); + await expect(reader.prepare()).rejects.toThrow(/reader is closed/u); + await expect(reader.close()).resolves.toBeUndefined(); + } finally { + await Promise.allSettled([preparation]); + await reader.close(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(process.platform === "win32")( + "maps cancellation during native preparation before Electron path resolution", + async () => { + const directory = await mkdtemp( + join(tmpdir(), "rea-electron-token-abort-test-"), + ); + const fakeCompiler = await writeBlockingCompiler(directory); + const reader = createDarwinProcessRunTokenReader({ + xcrun: fakeCompiler.executable, + }); + const prepareSpy = vi + .spyOn(systemProcessOwnershipHost, "prepare") + .mockImplementation(async (signal) => { + await reader.prepare(signal); + }); + const launchSpy = vi.spyOn(electron, "launch"); + const controller = new AbortController(); + let captureOutcome: Promise | undefined; + try { + const input = electronActiveObservationInputSchema.parse({ + executable_path: "/missing/electron", + application_path: "/missing/application.js", + }); + captureOutcome = new PlaywrightElectronActiveProvider() + .capture(input, { signal: controller.signal }) + .then( + (value) => ({ state: "result" as const, value }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + const compileState = await waitForCompileState(fakeCompiler.statePath); + controller.abort(); + const outcome = await captureOutcome; + + expect(outcome.state).toBe("result"); + if (outcome.state !== "result") throw outcome.cause; + expect(outcome.value).toMatchObject({ + ok: false, + error: { reason: "cancelled", userCategory: "cancelled" }, + }); + expect(launchSpy).not.toHaveBeenCalled(); + await expect(access(compileState.root)).rejects.toThrow(); + } finally { + if (!controller.signal.aborted) controller.abort(); + if (captureOutcome !== undefined) await captureOutcome; + prepareSpy.mockRestore(); + launchSpy.mockRestore(); + await reader.close(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(process.platform === "win32")( + "preserves cancellation through system-host baseline identity inspection", + async () => { + const directory = await mkdtemp( + join(tmpdir(), "rea-process-baseline-abort-test-"), + ); + const fakeCompiler = await writeBlockingIdentityCompiler(directory); + const host = createSystemProcessOwnershipHost("darwin", process.env, { + darwinXcrun: fakeCompiler.executable, + }); + const controller = new AbortController(); + let helperRoot: string | undefined; + const baseline = + host.captureBaseline?.(controller.signal).then( + () => ({ state: "fulfilled" as const }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ) ?? Promise.resolve({ state: "missing" as const }); + try { + if (host.captureBaseline === undefined) + throw new Error("Darwin host did not expose process baselines"); + const helper = await waitForIdentityHelper(fakeCompiler.statePath); + helperRoot = helper.root; + controller.abort(); + const outcome = await baseline; + + expect(outcome.state).toBe("rejected"); + if (outcome.state !== "rejected") + throw new Error("aborted identity inspection unexpectedly succeeded"); + expect(outcome.cause).toMatchObject({ name: "AbortError" }); + expect( + normalizeCaptureFailure(outcome.cause, controller.signal), + ).toMatchObject({ reason: "cancelled", userCategory: "cancelled" }); + await expect(access(helper.root)).resolves.toBeUndefined(); + } finally { + if (!controller.signal.aborted) controller.abort(); + await baseline; + await host.close?.(); + if (helperRoot !== undefined) + await expect(access(helperRoot)).rejects.toThrow(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(process.platform === "win32")( + "passes Electron cancellation into capture-local baseline inspection", + async () => { + const directory = await mkdtemp( + join(tmpdir(), "rea-electron-baseline-abort-test-"), + ); + const fakeCompiler = await writeBlockingIdentityCompiler(directory); + const reader = createDarwinProcessRunTokenReader({ + xcrun: fakeCompiler.executable, + }); + const prepareSpy = vi + .spyOn(systemProcessOwnershipHost, "prepare") + .mockResolvedValue(); + const baselineSpy = vi + .spyOn(systemProcessOwnershipHost, "captureBaseline") + .mockImplementation(async (signal) => { + await reader.identities( + [ + { + pid: process.pid, + parentPid: process.ppid, + processGroupId: process.pid, + state: "S", + command: process.execPath, + }, + ], + signal, + ); + return []; + }); + const launchSpy = vi.spyOn(electron, "launch"); + const controller = new AbortController(); + let captureOutcome: Promise | undefined; + try { + const input = electronActiveObservationInputSchema.parse({ + executable_path: process.execPath, + application_path: fileURLToPath(import.meta.url), + application_root: process.cwd(), + }); + captureOutcome = new PlaywrightElectronActiveProvider() + .capture(input, { signal: controller.signal }) + .then( + (value) => ({ state: "result" as const, value }), + (cause: unknown) => ({ state: "rejected" as const, cause }), + ); + const helper = await waitForIdentityHelper(fakeCompiler.statePath); + controller.abort(); + const outcome = await captureOutcome; + + expect(outcome.state).toBe("result"); + if (outcome.state !== "result") throw outcome.cause; + expect(outcome.value).toMatchObject({ + ok: false, + error: { reason: "cancelled", userCategory: "cancelled" }, + }); + expect(launchSpy).not.toHaveBeenCalled(); + await expect(access(helper.root)).resolves.toBeUndefined(); + } finally { + if (!controller.signal.aborted) controller.abort(); + if (captureOutcome !== undefined) await captureOutcome; + prepareSpy.mockRestore(); + baselineSpy.mockRestore(); + launchSpy.mockRestore(); + await reader.close(); + await rm(directory, { recursive: true, force: true }); + } + }, +); + +it.skipIf(!onDarwin)( + "parses exact NUL-delimited token records and rejects truncated argv", + async () => { + const root = await mkdtemp( + join(tmpdir(), "rea-process-token-parser-test-"), + ); + const executable = join(root, "parser-test"); + const core = fileURLToPath( + new URL( + "../../../bridge/process/ProcessRunTokenReader.swift", + import.meta.url, + ), + ); + const fixture = fileURLToPath( + new URL( + "../../fixtures/processRunTokenParserProbe.swift", + import.meta.url, + ), + ); + try { + await execFileOutput( + "/usr/bin/xcrun", + [ + "swiftc", + "-module-cache-path", + join(root, "modules"), + core, + fixture, + "-o", + executable, + ], + { timeout: 60_000 }, + ); + const { stdout } = await execFileAsync(executable, [], { + encoding: "utf8", + timeout: 10_000, + }); + expect(JSON.parse(stdout)).toEqual({ + argvDecoyIgnored: true, + alternatePaddingRead: true, + appleVectorTokenFailsClosed: true, + clearedAppleVectorTokenFailsClosed: true, + callerPfzBeforeTokenRead: true, + emptyLaterArgumentRead: true, + emptyEnvironmentFailsClosed: true, + emptyArgv0Read: true, + emptyEnvironmentRecordsBeforeTokenFailClosed: true, + duplicateTokenFailsClosed: true, + finalAssignmentArgumentRead: true, + missingAppleBoundaryFailsClosed: true, + nonAsciiPathRead: true, + realTokenRead: true, + reservedArgumentIgnored: true, + truncatedArgvFailsClosed: true, + zeroArgumentsRead: true, + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); + +it.skipIf(!onDarwin)( + "retries a growing process table after ENOMEM and preserves other sysctl errors", + async () => { + const root = await mkdtemp( + join(tmpdir(), "rea-process-snapshot-retry-test-"), + ); + const executable = join(root, "snapshot-probe"); + const core = fileURLToPath( + new URL( + "../../../bridge/process/ProcessRunTokenReader.swift", + import.meta.url, + ), + ); + const fixture = fileURLToPath( + new URL( + "../../fixtures/processRunTokenSnapshotProbe.swift", + import.meta.url, + ), + ); + try { + await execFileOutput( + "/usr/bin/xcrun", + [ + "swiftc", + "-module-cache-path", + join(root, "modules"), + core, + fixture, + "-o", + executable, + ], + { timeout: 60_000 }, + ); + const { stdout } = await execFileAsync(executable, [], { + encoding: "utf8", + timeout: 10_000, + }); + expect(JSON.parse(stdout)).toEqual({ + preservedOtherErrno: true, + retriedAfterFreshSize: true, + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); + +it.skipIf(!onDarwin)( + "reads only the exact ownership key from live process environments", + async () => { + const host = createSystemProcessOwnershipHost("darwin"); + const decoy = await startNodeChild({ + DECOY: "words REA_PROCESS_RUN_ID=synthetic-decoy", + }); + const owned = await startNodeChild({ + REA_PROCESS_RUN_ID: "synthetic-owned", + }); + try { + const processes = await host.listProcesses(); + const entries = [decoy, owned].map(({ pid }) => { + const entry = processes.find((process) => process.pid === pid); + if (entry === undefined) + throw new Error("test child is absent from process table"); + return entry; + }); + const observations = await host.runTokens?.(entries); + const identities = await host.processIdentities?.(entries); + expect(observations?.get(decoy.pid)).toEqual({ + state: "readable", + runId: undefined, + }); + expect(observations?.get(owned.pid)).toEqual({ + state: "readable", + runId: "synthetic-owned", + }); + expect(identities?.get(decoy.pid)?.state).toBe("readable"); + expect(identities?.get(owned.pid)?.state).toBe("readable"); + } finally { + await stopNodeChild(decoy.child); + await stopNodeChild(owned.child); + await host.close?.(); + } + }, +); + +it.skipIf(!onDarwin)( + "reads owned tokens with empty later arguments across executable path padding", + async () => { + const root = await mkdtemp(join(tmpdir(), "rea-process-token-padding-")); + const host = createSystemProcessOwnershipHost("darwin"); + const children: Awaited>[] = []; + try { + const executables = [join(root, "n"), join(root, "node")]; + for (const executable of executables) + await symlink(process.execPath, executable); + for (let index = 0; index < executables.length; index += 1) { + const executable = executables[index]; + if (executable === undefined) + throw new Error("test executable is missing"); + children.push( + await startNodeChild( + { REA_PROCESS_RUN_ID: `synthetic-padding-${String(index)}` }, + executable, + ["-e", "setInterval(() => {}, 1_000)", "", "FOO=bar"], + index === 0 ? "" : undefined, + ), + ); + } + const processes = await host.listProcesses(); + const entries = children.map(({ pid }) => { + const entry = processes.find((process) => process.pid === pid); + if (entry === undefined) + throw new Error("owned test child is absent from process table"); + return entry; + }); + const observations = await host.runTokens?.(entries); + expect(children.map(({ pid }) => observations?.get(pid))).toEqual([ + { state: "readable", runId: "synthetic-padding-0" }, + { state: "readable", runId: "synthetic-padding-1" }, + ]); + } finally { + for (const { child } of children) await stopNodeChild(child); + await host.close?.(); + await rm(root, { recursive: true, force: true }); + } + }, +); + +const startNodeChild = async ( + environment: NodeJS.ProcessEnv, + command = process.execPath, + arguments_: readonly string[] = ["-e", "setInterval(() => {}, 1_000)"], + argv0?: string, +) => { + const child = spawn(command, [...arguments_], { + env: environment, + stdio: "ignore", + ...(argv0 === undefined ? {} : { argv0 }), + }); + if (child.pid === undefined) + throw new Error("test child did not receive a PID"); + await new Promise((resolve, reject) => { + child.once("spawn", resolve); + child.once("error", reject); + }); + return { child, pid: child.pid }; +}; + +const stopNodeChild = async (child: ChildProcess) => { + if (child.exitCode !== null || child.signalCode !== null) return; + const exited = new Promise((resolve) => + child.once("exit", () => resolve()), + ); + child.kill("SIGKILL"); + await exited; +}; diff --git a/tests/boundary/process/filesystemSnapshotFifoRace.test.ts b/tests/boundary/process/filesystemSnapshotFifoRace.test.ts new file mode 100644 index 0000000000..5843158e3b --- /dev/null +++ b/tests/boundary/process/filesystemSnapshotFifoRace.test.ts @@ -0,0 +1,54 @@ +import { randomUUID } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { expect, it } from "vitest"; + +import { + ProviderProcessSupervisor, + spawnOwnedProviderProcess, +} from "../../../src/process/ProviderProcess.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it.skipIf(process.platform === "win32")( + "does not block when an observed regular file is replaced by a FIFO", + async () => { + const directory = await createTestTempDirectory("rea-snapshot-fifo-"); + const probe = fileURLToPath( + new URL( + "../../fixtures/filesystemSnapshotFifoProbe.mjs", + import.meta.url, + ), + ); + const snapshotModule = fileURLToPath( + new URL( + "../../../dist/process/capture/FilesystemSnapshot.js", + import.meta.url, + ), + ); + const launch = await spawnOwnedProviderProcess({ + command: process.execPath, + arguments: [probe, snapshotModule, directory], + runId: `rea-snapshot-fifo-${randomUUID()}`, + cwd: tmpdir(), + hostEnvironment: process.env, + }); + const supervisor = new ProviderProcessSupervisor({ + ...launch, + ownsProcessLifetime: true, + }); + + try { + const closed = await supervisor.waitForOutputClose(2_000); + expect(closed).toBe(true); + expect(supervisor.snapshot().stdout.text.trim()).toBe("rejected"); + } finally { + const stopped = await supervisor.stop({ + terminationGraceMs: 100, + killGraceMs: 500, + }); + expect(stopped.status).not.toBe("incomplete"); + } + }, +); diff --git a/tests/boundary/process/ownedCommand.test.ts b/tests/boundary/process/ownedCommand.test.ts new file mode 100644 index 0000000000..c40e5fd57c --- /dev/null +++ b/tests/boundary/process/ownedCommand.test.ts @@ -0,0 +1,186 @@ +import { randomUUID } from "node:crypto"; +import { setTimeout as delay } from "node:timers/promises"; +import { expect, it } from "vitest"; +import { + OwnedCommandFailure, + runOwnedCommand, +} from "../../../src/process/OwnedCommand.js"; +import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; +import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; +import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; + +const command = (script: string) => ({ + command: process.execPath, + arguments: ["-e", script], + runId: `rea-owned-command-test-${randomUUID()}`, +}); + +it("includes ownership preparation in the command deadline before provider creation", async () => { + let prepared = false; + await expect( + runOwnedCommand( + command("process.exit(0)"), + { + timeoutMs: 20, + diagnosticBytes: 1024, + }, + { + launcher: async (input) => { + await delay(80, undefined, { signal: input.signal }); + prepared = true; + throw new Error( + "Slow preparation must be aborted before provider creation", + ); + }, + }, + ), + ).rejects.toMatchObject({ reason: "timeout", snapshot: null }); + expect(prepared).toBe(false); +}); + +it("passes cancellation into native ownership preparation before provider creation", async () => { + const controller = new AbortController(); + await expect( + runOwnedCommand( + command("process.exit(0)"), + { timeoutMs: 2000, diagnosticBytes: 1024 }, + { + signal: controller.signal, + launcher: async (input) => { + expect(input.signal?.aborted).toBe(false); + controller.abort(); + input.signal?.throwIfAborted(); + throw new Error("Cancelled preparation must not launch a provider"); + }, + }, + ), + ).rejects.toMatchObject({ reason: "cancelled", snapshot: null }); +}); + +it("bounds retained diagnostics across both streams during a real output burst", async () => { + try { + await runOwnedCommand( + command( + 'process.stdout.write("a".repeat(256 * 1024)); process.stderr.write("b".repeat(256 * 1024))', + ), + { timeoutMs: 2_000, diagnosticBytes: 1024 }, + ); + throw new Error("An oversized producer must fail"); + } catch (cause: unknown) { + if (!(cause instanceof OwnedCommandFailure)) throw cause; + expect(cause.reason).toBe("output-limit"); + expect(cause.snapshot).not.toBeNull(); + expect( + (cause.snapshot?.stdout.bytes ?? 0) + (cause.snapshot?.stderr.bytes ?? 0), + ).toBeLessThanOrEqual(1024); + } +}); + +it("collects a real owned command and verifies its exit before returning diagnostics", async () => { + const result = await runOwnedCommand( + command('process.stdout.write("observed-output")'), + { timeoutMs: 2_000, diagnosticBytes: 1024 }, + ); + expect(result).toMatchObject({ + exitCode: 0, + signal: null, + stdout: { text: "observed-output", bytes: 15 }, + }); +}); + +it("cancels a real acquired process and independently releases it", async () => { + const controller = new AbortController(); + let pid: number | undefined; + const result = runOwnedCommand( + command( + 'process.stdout.write("ready\\n"); setInterval(() => undefined, 1000)', + ), + { timeoutMs: 5_000, diagnosticBytes: 1024 }, + { + signal: controller.signal, + launcher: async (input) => { + const process = await spawnOwnedProviderProcess(input); + pid = process.process.pid; + await waitForProviderProcessReady(process.process); + setImmediate(() => controller.abort()); + return process; + }, + }, + ); + await expect(result).rejects.toMatchObject({ + reason: "cancelled", + cleanupFailure: null, + }); + if (pid === undefined) throw new Error("Process was not acquired"); + const acquiredPid = pid; + expect(() => process.kill(acquiredPid, 0)).toThrow(); +}); + +it("returns a timeout for a running process after independent cleanup", async () => { + await expect( + runOwnedCommand(command("setInterval(() => undefined, 1000)"), { + timeoutMs: 50, + diagnosticBytes: 1024, + }), + ).rejects.toMatchObject({ reason: "timeout", cleanupFailure: null }); +}); + +it("honors cancellation during cleanup after a successful command", async () => { + const controller = new AbortController(); + await expect( + runOwnedCommand( + command("process.exit(0)"), + { timeoutMs: 2_000, diagnosticBytes: 1024 }, + { + signal: controller.signal, + launcher: async (input) => { + const launched = await spawnOwnedProviderProcess(input); + return { + ...launched, + cleanup: async () => { + const result = await cleanupOwnedProcessGroup(launched.ownership); + controller.abort(); + return result; + }, + }; + }, + }, + ), + ).rejects.toMatchObject({ reason: "cancelled", cleanupFailure: null }); +}); + +it("rejects oversized diagnostics even when the child exits immediately", async () => { + await expect( + runOwnedCommand(command('process.stdout.write("x".repeat(2048))'), { + timeoutMs: 2_000, + diagnosticBytes: 1024, + }), + ).rejects.toMatchObject({ reason: "output-limit", cleanupFailure: null }); +}); + +it("preserves original command failure together with failed cleanup resource identities", async () => { + const request = command("process.exit(2)"); + await expect( + runOwnedCommand( + request, + { timeoutMs: 2_000, diagnosticBytes: 1024 }, + { + launcher: async (input) => { + const process = await spawnOwnedProviderProcess(input); + return { + ...process, + cleanup: async () => ({ + cleaned: false as const, + reason: "synthetic cleanup could not be confirmed", + }), + }; + }, + }, + ), + ).rejects.toMatchObject({ + reason: "process", + cleanupFailure: "synthetic cleanup could not be confirmed", + resources: expect.arrayContaining([request.runId]), + snapshot: { exitCode: 2 }, + }); +}); diff --git a/tests/boundary/process/processCaptureCapability.test.ts b/tests/boundary/process/processCaptureCapability.test.ts index 5a8e311506..23d0219c92 100644 --- a/tests/boundary/process/processCaptureCapability.test.ts +++ b/tests/boundary/process/processCaptureCapability.test.ts @@ -4,7 +4,7 @@ import { processCaptureProbeFailureReason, processCaptureOwnershipUnavailableReason, probeProcessCaptureCapability, -} from "../../../src/application/ProcessCaptureCapability.js"; +} from "../../../src/process/capture/ProcessCaptureCapability.js"; describe("process capture capability diagnostics", () => { it("distinguishes missing native modules from runtime ABI mismatches", () => { @@ -52,6 +52,31 @@ describe("process capture capability diagnostics", () => { ).toContain("spawn EACCES"); }); + it("prepares Darwin ownership inspection before probing or admitting a PTY", async () => { + let loaded = false; + const capability = await probeProcessCaptureCapability({ + platform: "darwin", + prepareOwnershipInspector: async () => { + throw new Error( + "macOS process ownership inspection requires the Apple Swift compiler via xcrun", + ); + }, + loadPty: async () => { + loaded = true; + return await import("@lydell/node-pty"); + }, + }); + + expect(loaded).toBe(false); + expect(capability).toEqual({ + available: false, + backend: "node-pty", + reason: expect.stringContaining( + "requires the Apple Swift compiler via xcrun", + ), + }); + }); + it("keeps the ownership limitation separate from native PTY support", () => { expect(processCaptureOwnershipUnavailableReason("win32")).toContain( "Windows PTY process capture", diff --git a/tests/boundary/process/processCaptureCapability.ts b/tests/boundary/process/processCaptureCapability.ts index 4b345d7341..3773ff88f0 100644 --- a/tests/boundary/process/processCaptureCapability.ts +++ b/tests/boundary/process/processCaptureCapability.ts @@ -1,6 +1,6 @@ import { it } from "vitest"; -import { probeProcessCaptureCapability } from "../../../src/application/ProcessCaptureCapability.js"; +import { probeProcessCaptureCapability } from "../../../src/process/capture/ProcessCaptureCapability.js"; /** * Process-capture tests need real native PTY authority. Probing once at module diff --git a/tests/boundary/process/processCaptureLifecycle.test.ts b/tests/boundary/process/processCaptureLifecycle.test.ts index 6e16c4524d..53e42aaa6f 100644 --- a/tests/boundary/process/processCaptureLifecycle.test.ts +++ b/tests/boundary/process/processCaptureLifecycle.test.ts @@ -1,5 +1,6 @@ +import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; -import { rm, symlink } from "node:fs/promises"; +import { access, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -7,14 +8,378 @@ import { expect } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { itWithCaptureCapability } from "./processCaptureCapability.js"; -import { captureProcessScenario } from "../../../src/application/ProcessHarness.js"; -import { parseProcessScenario } from "../../../src/domain/processCapture.js"; +import { captureProcessScenario } from "../../../src/process/capture/ProcessHarness.js"; +import { snapshotRoots } from "../../../src/process/capture/FilesystemSnapshot.js"; +import { ProcessCaptureError } from "../../../src/process/capture/ProcessCaptureError.js"; +import { + type ProcessCaptureCleanupHost, + observeLaunchedExecutable, + observeSelectedExecutable, +} from "../../../src/process/capture/ProcessCaptureLifecycle.js"; +import { + parseProcessScenario, + type ProcessCapture, +} from "../../../src/domain/process/processCapture.js"; const processFixture = fileURLToPath( new URL("../../fixtures/processFidelity.mjs", import.meta.url), ); +const snapshotCancellationFixture = fileURLToPath( + new URL("../../fixtures/processSnapshotCancellation.mjs", import.meta.url), +); const execFileAsync = promisify(execFile); +type CaptureRun = Awaited>; +type PartialCapture = Extract< + NonNullable, + { readonly capture: unknown } +>["capture"]; +const captureObservations = ( + result: CaptureRun, +): { + readonly capture: ProcessCapture | PartialCapture; + readonly cleanupIncomplete: boolean; +} => { + if (result.ok) return { capture: result.value, cleanupIncomplete: false }; + if (!(result.error instanceof ProcessCaptureError)) throw result.error; + const report = result.error.cleanupReport; + const partial = result.error.partialObservation; + expect(result.error.reason).toBe("cleanup_incomplete"); + expect(report?.owned_process_group.state).toBe("unverified"); + const [summary, breakdown] = (report?.owned_process_group.reason ?? "").split( + ": ", + ); + expect(summary).toMatch( + /^process ownership token could not be read for [1-9][0-9]* live process\(es\)$/u, + ); + const categories = breakdown?.split(", ") ?? []; + expect(categories.length).toBeGreaterThan(0); + const expectedCategory = + process.platform === "linux" + ? /^environment_errno_(?:EACCES|EPERM)=[1-9][0-9]*$/u + : /^environment_unavailable=[1-9][0-9]*$/u; + for (const category of categories) expect(category).toMatch(expectedCategory); + expect(report?.terminal_renderer.state).toBe("cleaned"); + expect(report?.temporary_root.state).toBe("cleaned"); + if (partial === undefined || !("capture" in partial)) + throw new Error( + "cleanup-incomplete capture omitted completed observations", + ); + const capture = partial.capture; + expect(capture.settlement.cleanup_outcome).toBe("failed"); + return { capture, cleanupIncomplete: true }; +}; + +const processState = async (pid: number): Promise => { + try { + const { stdout } = await execFileAsync("ps", [ + "-o", + "stat=", + "-p", + String(pid), + ]); + return stdout.trim() || undefined; + } catch (cause: unknown) { + if ( + cause instanceof Error && + "code" in cause && + cause.code === 1 && + "stdout" in cause && + cause.stdout === "" + ) + return undefined; + throw cause; + } +}; + +const waitForProcessExit = async (pid: number): Promise => { + const deadline = Date.now() + 2_000; + while (Date.now() < deadline) { + const state = await processState(pid); + if (state === undefined || state.startsWith("Z")) return; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw new Error(`Process ${String(pid)} did not exit within the test bound`); +}; + +const readDetachedFixturePids = async (marker: string): Promise => { + let contents: string; + try { + contents = await readFile(marker, "utf8"); + } catch (cause: unknown) { + if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") + return []; + throw cause; + } + const parsed: unknown = JSON.parse(contents); + if (typeof parsed !== "object" || parsed === null) return []; + const candidatePids = [ + "ownedPid" in parsed ? parsed.ownedPid : undefined, + "unownedPid" in parsed ? parsed.unownedPid : undefined, + ]; + return candidatePids.flatMap((pid) => + typeof pid === "number" && Number.isSafeInteger(pid) && pid > 1 + ? [pid] + : [], + ); +}; + +const terminateFixtureProcess = async (pid: number): Promise => { + try { + process.kill(pid, "SIGKILL"); + } catch (cause: unknown) { + if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) + throw cause; + } + await waitForProcessExit(pid); +}; + +itWithCaptureCapability( + "cleans detached run-token children omitted by sampling and preserves an unowned neighbor", + async () => { + const root = await createTestTempDirectory("rea-detached-cleanup-"); + const pidMarker = join(root, "children.json"); + let ownedPid: number | undefined; + let unownedPid: number | undefined; + try { + const result = await captureProcessScenario( + parseProcessScenario({ + executable: process.execPath, + arguments: [processFixture, "detached-cleanup", pidMarker], + working_directory: root, + settle_ms: 0, + timeout_ms: 5_000, + idle_timeout_ms: 5_000, + limits: { processes: 1 }, + }), + ); + const { capture, cleanupIncomplete } = captureObservations(result); + const parsed: unknown = JSON.parse(await readFile(pidMarker, "utf8")); + if ( + typeof parsed !== "object" || + parsed === null || + !("ownedPid" in parsed) || + typeof parsed.ownedPid !== "number" || + !("unownedPid" in parsed) || + typeof parsed.unownedPid !== "number" + ) + throw new TypeError("detached-child fixture wrote invalid PIDs"); + // Register fixture children before assertions so teardown can clean them + // even when a later expectation fails. + ownedPid = parsed.ownedPid; + unownedPid = parsed.unownedPid; + expect(capture.process_samples.some(({ pid }) => pid === ownedPid)).toBe( + false, + ); + if (cleanupIncomplete) + expect(capture.settlement.cleanup_outcome).toBe("failed"); + else + expect(result.ok && result.value.cleanup.owned_process_group).toBe( + "verified", + ); + await waitForProcessExit(ownedPid); + const unownedState = await processState(unownedPid); + expect(unownedState).toBeDefined(); + expect(unownedState?.startsWith("Z")).toBe(false); + } finally { + try { + let fixturePids: number[] = []; + let teardownFailure: unknown; + try { + fixturePids = await readDetachedFixturePids(pidMarker); + } catch (cause: unknown) { + teardownFailure = cause; + } + for (const pid of new Set([ + ...fixturePids, + ...(ownedPid === undefined ? [] : [ownedPid]), + ...(unownedPid === undefined ? [] : [unownedPid]), + ])) { + try { + await terminateFixtureProcess(pid); + } catch (cause: unknown) { + teardownFailure ??= cause; + } + } + if (teardownFailure !== undefined) throw teardownFailure; + } finally { + await rm(root, { recursive: true, force: true }); + } + } + }, + 10_000, +); + +itWithCaptureCapability( + "retains collected observations when completion and cleanup both fail", + async () => { + let snapshotCalls = 0; + const captureSnapshot: typeof snapshotRoots = async () => { + snapshotCalls += 1; + if (snapshotCalls === 1) return { files: [], truncated: false }; + throw new Error("fixture final snapshot failure"); + }; + const cleanupHost: ProcessCaptureCleanupHost = { + platform: process.platform, + cleanupProcessGroup: async () => ({ + cleaned: false, + reason: "fixture cleanup could not be verified", + }), + verifyTokenOwnedProcesses: async () => ({ + cleaned: false, + reason: "fixture cleanup could not be verified", + }), + removeTemporaryRoot: async (path) => + rm(path, { recursive: true, force: true }), + }; + const result = await captureProcessScenario( + parseProcessScenario({ + executable: process.execPath, + arguments: [processFixture, "partial"], + working_directory: dirname(processFixture), + settle_ms: 0, + timeout_ms: 5_000, + idle_timeout_ms: 5_000, + }), + undefined, + process.platform, + process.env, + captureSnapshot, + cleanupHost, + ); + + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected cleanup-incomplete failure"); + if (!(result.error instanceof ProcessCaptureError)) throw result.error; + const partial = result.error.partialObservation; + expect(result.error.reason).toBe("cleanup_incomplete"); + expect(result.error.executionFailure).toBe( + "fixture final snapshot failure", + ); + expect(partial).toBeDefined(); + if (partial === undefined || !("observations" in partial)) + throw new Error("expected incomplete process observations"); + const observations = partial.observations; + expect(observations.frames.state).toBe("available"); + if (observations.frames.state !== "available") + throw new Error("expected terminal output observations"); + expect( + observations.frames.value.map(({ data }) => data).join(""), + ).toContain("partial-frame"); + expect(observations.rendered_frames.state).toBe("available"); + if (observations.rendered_frames.state !== "available") + throw new Error("expected rendered terminal observations"); + expect(observations.rendered_frames.value.length).toBeGreaterThan(0); + expect(observations.interaction_events.state).toBe("available"); + expect(observations.exit).toMatchObject({ + state: "available", + value: { reason: "exited" }, + }); + expect(observations.settlement.state).toBe("available"); + expect(observations.process_samples.state).toBe("available"); + expect(observations.target_pid).toEqual({ state: "available", value: 1 }); + expect(observations.filesystem_snapshots.before.state).toBe("available"); + if (observations.filesystem_snapshots.before.state !== "available") + throw new Error("expected initial filesystem snapshot"); + expect(observations.filesystem_snapshots.before.value.truncated).toBe( + false, + ); + expect(observations.filesystem_snapshots.after).toEqual({ + state: "unavailable", + reason: + "Final filesystem snapshot failed: fixture final snapshot failure", + }); + expect(observations.event_journal.state).toBe("available"); + if (observations.event_journal.state !== "available") + throw new Error("expected capture event journal"); + expect( + observations.event_journal.value.some( + ({ collection }) => collection === "lifecycle", + ), + ).toBe(true); + expect(observations.manifest.state).toBe("unavailable"); + expect(snapshotCalls).toBe(2); + }, + 10_000, +); + +itWithCaptureCapability( + "keeps selected executable evidence separate when the path identity changes before validation", + async () => { + const root = await createTestTempDirectory("rea-executable-replaced-"); + const executable = join(root, "selected-executable"); + await symlink(process.execPath, executable); + try { + const selected = await observeSelectedExecutable(executable); + expect(selected.sha256).toMatch(/^[a-f0-9]{64}$/u); + await rm(executable); + await symlink("/bin/sh", executable); + expect(observeLaunchedExecutable(executable, selected)).toEqual({ + selectedSha256: selected.sha256, + sha256: null, + state: "unknown", + reason: "Executable path metadata changed between sampling and spawn.", + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); + +itWithCaptureCapability( + "keeps the prelaunch executable sample when the selected path is replaced during capture", + async () => { + const root = await createTestTempDirectory("rea-executable-identity-"); + const executable = join(root, "selected-executable"); + const readyMarker = join(root, "child-ready"); + await symlink(process.execPath, executable); + const launchedDigest = createHash("sha256") + .update(await readFile(process.execPath)) + .digest("hex"); + try { + const capturePromise = captureProcessScenario( + parseProcessScenario({ + executable, + arguments: [processFixture, "ready-hang", readyMarker], + working_directory: root, + timeout_ms: 1_500, + idle_timeout_ms: 5_000, + }), + ); + const deadline = Date.now() + 2_000; + let ready = false; + while (!ready && Date.now() < deadline) { + try { + await access(readyMarker); + ready = true; + } catch { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + expect(ready).toBe(true); + await rm(executable); + await symlink("/bin/sh", executable); + const result = await capturePromise; + const { capture } = captureObservations(result); + expect(capture.manifest).toMatchObject({ + selected_executable_sha256: launchedDigest, + executable_sha256: launchedDigest, + executable_identity: { + state: "path_metadata_unchanged", + reason: null, + }, + }); + expect(capture.exit.reason).toBe("timeout"); + expect(capture.limitations).toContain( + "The executable digest is a prelaunch file sample; matching path metadata immediately after spawn does not prove an atomic operating-system image binding.", + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, + 10_000, +); + itWithCaptureCapability( "records external symlink metadata without following the target", async () => { @@ -28,16 +393,15 @@ itWithCaptureCapability( filesystem_observation_paths: [root], }), ); - expect(result.ok).toBe(true); - if (!result.ok) throw result.error; - const escaped = result.value.files_after.find((file) => + const { capture } = captureObservations(result); + const escaped = capture.files_after.find((file) => file.path.endsWith(":escape"), ); expect(escaped?.symlink_target).toBe("/etc/passwd"); - expect(result.value.truncated).toBe(false); - expect(JSON.stringify(result.value.files_after)).not.toContain(root); + expect(capture.truncated).toBe(false); + expect(JSON.stringify(capture.files_after)).not.toContain(root); expect( - result.value.files_after.some((file) => file.path.includes("passwd")), + capture.files_after.some((file) => file.path.includes("passwd")), ).toBe(false); } finally { await rm(root, { recursive: true, force: true }); @@ -57,13 +421,13 @@ itWithCaptureCapability( idle_timeout_ms: 5_000, }), ); - expect(timedOut.ok).toBe(true); - if (!timedOut.ok) throw timedOut.error; - expect(timedOut.value.exit.reason).toBe("timeout"); - expect(timedOut.value.cleanup).toEqual({ - owned_process_group: "verified", - temporary_root: "removed", - }); + const timedOutCapture = captureObservations(timedOut); + expect(timedOutCapture.capture.exit.reason).toBe("timeout"); + if (!timedOutCapture.cleanupIncomplete && timedOut.ok) + expect(timedOut.value.cleanup).toEqual({ + owned_process_group: "verified", + temporary_root: "removed", + }); const controller = new AbortController(); setTimeout(() => controller.abort(), 50); @@ -83,6 +447,114 @@ itWithCaptureCapability( }, ); +itWithCaptureCapability( + "classifies cancellation raised by the initial filesystem snapshot", + async () => { + const root = await createTestTempDirectory("rea-snapshot-initial-cancel-"); + const path = join(root, "input"); + await writeFile(path, "observed\n"); + const controller = new AbortController(); + const { signal } = controller; + const throwIfAborted = signal.throwIfAborted.bind(signal); + Object.defineProperty(signal, "throwIfAborted", { + value: () => { + controller.abort(); + throwIfAborted(); + }, + }); + + const result = await captureProcessScenario( + parseProcessScenario({ + executable: process.execPath, + working_directory: root, + filesystem_observation_paths: [path], + }), + signal, + ); + + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected initial snapshot cancellation"); + expect(result.error).toMatchObject({ + reason: "cancelled", + userCategory: "cancelled", + }); + }, +); + +itWithCaptureCapability( + "classifies cancellation raised by the final filesystem snapshot", + async () => { + const root = await createTestTempDirectory("rea-snapshot-final-cancel-"); + const controller = new AbortController(); + let initialSnapshotCompleted = false; + const captureSnapshot: typeof snapshotRoots = async (scenario, signal) => { + if (initialSnapshotCompleted) controller.abort(); + const snapshot = await snapshotRoots(scenario, signal); + initialSnapshotCompleted = true; + return snapshot; + }; + const resultPromise = captureProcessScenario( + parseProcessScenario({ + executable: process.execPath, + arguments: [snapshotCancellationFixture, root], + working_directory: dirname(snapshotCancellationFixture), + filesystem_observation_paths: [root], + settle_ms: 0, + }), + controller.signal, + process.platform, + process.env, + captureSnapshot, + ); + + const result = await resultPromise; + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected final snapshot cancellation"); + expect(initialSnapshotCompleted).toBe(true); + expect(result.error).toMatchObject({ + reason: "cancelled", + userCategory: "cancelled", + }); + }, +); + +itWithCaptureCapability( + "preserves filesystem errors that are not caller cancellation", + async () => { + const root = await createTestTempDirectory("rea-snapshot-io-error-"); + const obstruction = join(root, "not-a-directory"); + await writeFile(obstruction, "file"); + const controller = new AbortController(); + const captureSnapshot: typeof snapshotRoots = async (scenario, signal) => { + try { + return await snapshotRoots(scenario, signal); + } catch (cause: unknown) { + controller.abort(); + throw cause; + } + }; + + const result = await captureProcessScenario( + parseProcessScenario({ + executable: process.execPath, + working_directory: root, + filesystem_observation_paths: [join(obstruction, "child")], + }), + controller.signal, + process.platform, + process.env, + captureSnapshot, + ); + + expect(result.ok).toBe(false); + if (result.ok) throw new Error("expected filesystem observation failure"); + expect(result.error).toMatchObject({ + reason: "capture_failed", + cause: { code: "ENOTDIR" }, + }); + }, +); + itWithCaptureCapability( "captures input, Unicode, and resize across PTY startup", async () => { @@ -100,17 +572,16 @@ itWithCaptureCapability( idle_timeout_ms: 10_000, }), ); - if (!result.ok) throw result.error; - expect(result.ok).toBe(true); - const output = result.value.frames.map(({ data }) => data).join(""); + const { capture } = captureObservations(result); + const output = capture.frames.map(({ data }) => data).join(""); expect(output).toContain("prompt>"); expect(output).toContain("input:answer unicode:雪"); expect(output).toContain("resize:100x40"); - expect(result.value.interaction_events).toMatchObject([ + expect(capture.interaction_events).toMatchObject([ { type: "resize", outcome: "dispatched", scheduled_at_ms: 0 }, { type: "input", outcome: "dispatched", scheduled_at_ms: 0 }, ]); - const resized = result.value.rendered_frames.find( + const resized = capture.rendered_frames.find( ({ columns, rows, lines }) => columns === 100 && rows === 40 && @@ -118,7 +589,7 @@ itWithCaptureCapability( ); expect(resized).toBeDefined(); expect(resized?.lines.join("\n")).toContain("input:answer unicode:雪"); - expect(result.value.exit.code).toBe(0); + expect(capture.exit.code).toBe(0); }, ); @@ -135,12 +606,11 @@ itWithCaptureCapability( idle_timeout_ms: 2_000, }), ); - expect(result.ok).toBe(true); - if (!result.ok) throw result.error; - expect(result.value.interaction_events).toMatchObject([ + const { capture } = captureObservations(result); + expect(capture.interaction_events).toMatchObject([ { type: "signal", data: "SIGTERM", outcome: "dispatched" }, ]); - expect(result.value.exit).toMatchObject({ signal: 15, reason: "exited" }); + expect(capture.exit).toMatchObject({ signal: 15, reason: "exited" }); }, ); @@ -160,24 +630,22 @@ itWithCaptureCapability( idle_timeout_ms: 20_000, }), ); - expect(result.ok).toBe(true); - if (!result.ok) throw result.error; - expect(result.value.frames.map(({ data }) => data).join("")).toContain( + const { capture } = captureObservations(result); + expect(capture.frames.map(({ data }) => data).join("")).toContain( "input:answer", ); - expect(result.value.interaction_events).toMatchObject([ + expect(capture.interaction_events).toMatchObject([ { type: "resize", scheduled_at_ms: 25, outcome: "dispatched" }, { type: "input", scheduled_at_ms: 50, outcome: "dispatched" }, ]); expect( - result.value.interaction_events.every( + capture.interaction_events.every( ({ scheduled_at_ms, dispatched_at_ms }) => dispatched_at_ms >= scheduled_at_ms, ), ).toBe(true); expect( - result.value.frames.find(({ data }) => data.includes("input:answer")) - ?.at_ms, + capture.frames.find(({ data }) => data.includes("input:answer"))?.at_ms, ).toBeGreaterThanOrEqual(50); }, ); @@ -194,13 +662,12 @@ itWithCaptureCapability( idle_timeout_ms: 20_000, }), ); - expect(result.ok).toBe(true); - if (!result.ok) throw result.error; - expect(result.value.exit).toMatchObject({ code: 0, reason: "exited" }); - expect(result.value.frames.map(({ data }) => data).join("")).toContain( + const { capture } = captureObservations(result); + expect(capture.exit).toMatchObject({ code: 0, reason: "exited" }); + expect(capture.frames.map(({ data }) => data).join("")).toContain( "tree-ready", ); - const commands = result.value.process_samples.map(({ command }) => command); + const commands = capture.process_samples.map(({ command }) => command); expect(commands.some((command) => command.includes("tree-child"))).toBe( true, ); @@ -210,7 +677,7 @@ itWithCaptureCapability( expect( commands.some((command) => command.includes("tree-grandchild")), ).toBe(true); - expect(JSON.stringify(result.value.process_samples)).toContain( + expect(JSON.stringify(capture.process_samples)).toContain( dirname(processFixture), ); const { stdout } = await execFileAsync("ps", ["-axo", "command="]); diff --git a/tests/boundary/process/processCaptureNormalization.test.ts b/tests/boundary/process/processCaptureNormalization.test.ts index 4ff716b180..dbaef7c28c 100644 --- a/tests/boundary/process/processCaptureNormalization.test.ts +++ b/tests/boundary/process/processCaptureNormalization.test.ts @@ -4,13 +4,13 @@ import { fileURLToPath } from "node:url"; import { expect, it } from "vitest"; import { z } from "zod"; -import { captureProcessScenario } from "../../../src/application/ProcessHarness.js"; -import { createProcessCaptureEvidence } from "../../../src/application/ProcessEvidence.js"; +import { captureProcessScenario } from "../../../src/process/capture/ProcessHarness.js"; +import { createProcessCaptureEvidence } from "../../../src/application/process/ProcessEvidence.js"; import { digestProcessCommitment, parseProcessCapture, parseProcessScenario, -} from "../../../src/domain/processCapture.js"; +} from "../../../src/domain/process/processCapture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { itWithCaptureCapability } from "./processCaptureCapability.js"; @@ -41,7 +41,6 @@ itWithLinuxCaptureCapability.each([false, true])( idle_timeout_ms: 10_000, }); const result = await captureProcessScenario(scenario); - expect(result.ok).toBe(true); if (!result.ok) throw result.error; const capture = result.value; const identity = identitySchema.parse( diff --git a/tests/boundary/process/processCaptureTerminal.test.ts b/tests/boundary/process/processCaptureTerminal.test.ts index 028dd94fd7..500d83c6b2 100644 --- a/tests/boundary/process/processCaptureTerminal.test.ts +++ b/tests/boundary/process/processCaptureTerminal.test.ts @@ -6,8 +6,8 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { captureProcessScenario, probeProcessCaptureCapability, -} from "../../../src/application/ProcessHarness.js"; -import { parseProcessScenario } from "../../../src/domain/processCapture.js"; +} from "../../../src/process/capture/ProcessHarness.js"; +import { parseProcessScenario } from "../../../src/domain/process/processCapture.js"; it("captures one command's terminal output, selected files, and owned cleanup", async () => { const root = await createTestTempDirectory("rea-process-capture-"); diff --git a/tests/boundary/process/processHostIdentity.test.ts b/tests/boundary/process/processHostIdentity.test.ts index 816e78d043..995648ab5c 100644 --- a/tests/boundary/process/processHostIdentity.test.ts +++ b/tests/boundary/process/processHostIdentity.test.ts @@ -1,10 +1,12 @@ import { expect, it } from "vitest"; -import { parseProcessScenario } from "../../../src/domain/processScenario.js"; +import { parseProcessScenario } from "../../../src/domain/process/processScenario.js"; import { createRunManifest, + observeLaunchedExecutable, + observeSelectedExecutable, observeSettlement, -} from "../../../src/application/ProcessCaptureLifecycle.js"; +} from "../../../src/process/capture/ProcessCaptureLifecycle.js"; it("uses one selected host identity for manifest and settlement", async () => { const scenario = parseProcessScenario({ @@ -16,10 +18,14 @@ it("uses one selected host identity for manifest and settlement", async () => { platform: "win32" as const, architecture: "arm64" as const, }; + const selectedExecutable = await observeSelectedExecutable( + scenario.executable, + ); const manifest = await createRunManifest( scenario, new Date(0), new Date(1), + observeLaunchedExecutable(scenario.executable, selectedExecutable), selectedHost, ); const settlement = await observeSettlement( diff --git a/tests/boundary/process/processPreparation.test.ts b/tests/boundary/process/processPreparation.test.ts new file mode 100644 index 0000000000..262a0cb260 --- /dev/null +++ b/tests/boundary/process/processPreparation.test.ts @@ -0,0 +1,62 @@ +import { expect, it, vi } from "vitest"; + +import { snapshotRoots } from "../../../src/process/capture/FilesystemSnapshot.js"; +import { prepareProcessCapture } from "../../../src/process/capture/ProcessCaptureLifecycle.js"; +import { parseProcessScenario } from "../../../src/domain/process/processCapture.js"; + +it("prepares ownership inspection before snapshotting or allocating a target run", async () => { + const failure = new Error("native process identity inspection unavailable"); + const createTemporaryRoot = vi.fn(async () => "/unused"); + const cleanup = vi.fn(async () => undefined); + const prepareOwnershipInspector = vi.fn(async () => { + throw failure; + }); + const captureSnapshot: typeof snapshotRoots = async () => { + throw new Error("snapshot must not run after failed ownership preflight"); + }; + + await expect( + prepareProcessCapture( + parseProcessScenario({ + executable: "/bin/true", + working_directory: "/tmp", + }), + undefined, + captureSnapshot, + { prepareOwnershipInspector, createTemporaryRoot, cleanup }, + ), + ).rejects.toBe(failure); + + expect(prepareOwnershipInspector).toHaveBeenCalledOnce(); + expect(createTemporaryRoot).not.toHaveBeenCalled(); + expect(cleanup).not.toHaveBeenCalled(); +}); + +it("cleans the temporary root when cancellation arrives during identity capture", async () => { + const controller = new AbortController(); + const temporaryRoot = "/temporary-capture-root"; + const cleanup = vi.fn(async () => undefined); + const host = { + createTemporaryRoot: vi.fn(async () => temporaryRoot), + captureOwnershipBaseline: vi.fn(async (signal?: AbortSignal) => { + expect(signal).toBe(controller.signal); + controller.abort(); + return []; + }), + cleanup, + }; + + await expect( + prepareProcessCapture( + parseProcessScenario({ + executable: "/bin/true", + working_directory: "/tmp", + }), + controller.signal, + async () => ({ files: [], truncated: false }), + host, + ), + ).rejects.toThrow(/cancelled/u); + expect(host.captureOwnershipBaseline).toHaveBeenCalledWith(controller.signal); + expect(cleanup).toHaveBeenCalledWith(temporaryRoot); +}); diff --git a/tests/boundary/process/providerProcess.test.ts b/tests/boundary/process/providerProcess.test.ts index 24f8179620..8e1440effd 100644 --- a/tests/boundary/process/providerProcess.test.ts +++ b/tests/boundary/process/providerProcess.test.ts @@ -8,7 +8,10 @@ import { PendingOperations } from "../../../src/process/PendingOperations.js"; import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; import { ProviderStartupDeadline } from "../../../src/process/ProviderDeadline.js"; import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; -import { observeOwnedProcessLineage } from "../../../src/process/ProcessOwnershipObservation.js"; +import { + observeOwnedProcessLineage, + systemProcessOwnershipHost, +} from "../../../src/process/ProcessOwnershipObservation.js"; import { ProviderProcessSupervisor, spawnOwnedProviderProcess, @@ -371,6 +374,52 @@ describe("provider process host injection", () => { }); }); +describe("provider startup cancellation", () => { + it("rejects an already cancelled launch before preparing or spawning", async () => { + const controller = new AbortController(); + const cancellation = new Error("provider launch already cancelled"); + controller.abort(cancellation); + + await expect( + spawnOwnedProviderProcess({ + command: `${tmpdir()}/rea-provider-must-not-spawn-after-cancel`, + arguments: [], + runId: "provider-process-pre-aborted-run", + signal: controller.signal, + }), + ).rejects.toBe(cancellation); + }); + + it.skipIf(process.platform !== "darwin")( + "forwards cancellation through native ownership preparation before spawning", + async () => { + const controller = new AbortController(); + const cancellation = new Error("cancelled during native preparation"); + const prepare = vi.spyOn(systemProcessOwnershipHost, "prepare"); + prepare.mockImplementation(async (signal) => { + expect(signal).toBe(controller.signal); + await new Promise((resolve) => { + signal?.addEventListener("abort", () => resolve(), { once: true }); + }); + }); + + try { + const starting = spawnOwnedProviderProcess({ + command: `${tmpdir()}/rea-provider-must-not-spawn-after-cancel`, + arguments: [], + runId: "provider-process-cancelled-preparation-run", + signal: controller.signal, + }); + controller.abort(cancellation); + await expect(starting).rejects.toBe(cancellation); + expect(prepare).toHaveBeenCalledOnce(); + } finally { + prepare.mockRestore(); + } + }, + ); +}); + describe("provider process spawning primitives", () => { it("spawns an owned process group with exact identity coordinates", async () => { const runId = "provider-process-run"; diff --git a/tests/boundary/process/providerProcessOutput.test.ts b/tests/boundary/process/providerProcessOutput.test.ts new file mode 100644 index 0000000000..888394c99b --- /dev/null +++ b/tests/boundary/process/providerProcessOutput.test.ts @@ -0,0 +1,42 @@ +import { expect, it } from "vitest"; +import { + ProviderProcessSupervisor, + type ProviderProcessDiagnostic, +} from "../../../src/process/ProviderProcess.js"; +import { + spawnProviderProcessFixture, + stopProviderProcessFixture, +} from "../../fixtures/providerProcess.js"; + +it("bounds capture before diagnostic callbacks while draining both output streams", async () => { + const child = spawnProviderProcessFixture("burst", 70_000); + const diagnostics: ProviderProcessDiagnostic[] = []; + const supervisor = new ProviderProcessSupervisor( + { process: child, ownsProcessLifetime: true }, + { + maxDiagnosticBytes: 1024, + onDiagnostic: (event) => { + const snapshot = supervisor.snapshot(); + expect( + snapshot.stdout.bytes + snapshot.stderr.bytes, + ).toBeLessThanOrEqual(1024); + diagnostics.push(event); + }, + }, + ); + try { + expect(await supervisor.waitForOutputClose(2_000)).toBe(true); + expect( + diagnostics.some((event) => event.type === "output" && event.truncated), + ).toBe(true); + for (const stream of ["stdout", "stderr"]) + expect( + diagnostics + .filter((event) => event.type === "output" && event.stream === stream) + .at(-1), + ).toMatchObject({ totalBytes: 70_000 }); + } finally { + await supervisor.stop(); + await stopProviderProcessFixture(child); + } +}); diff --git a/tests/boundary/process/selectedExecutableCancellation.test.ts b/tests/boundary/process/selectedExecutableCancellation.test.ts new file mode 100644 index 0000000000..238737a583 --- /dev/null +++ b/tests/boundary/process/selectedExecutableCancellation.test.ts @@ -0,0 +1,22 @@ +import { open, stat, type FileHandle } from "node:fs/promises"; +import { expect, it } from "vitest"; +import { observeSelectedExecutable } from "../../../src/process/capture/ProcessCaptureLifecycle.js"; + +it("cancels after real metadata IO without creating an already-aborted stream or leaking the file handle", async () => { + const controller = new AbortController(); + let opened: FileHandle | undefined; + await expect( + observeSelectedExecutable(process.execPath, controller.signal, { + async open(path, flags) { + opened = await open(path, flags); + return opened; + }, + async stat(path) { + const metadata = await stat(path, { bigint: true }); + controller.abort(); + return metadata; + }, + }), + ).rejects.toMatchObject({ reason: "cancelled" }); + expect(opened?.fd).toBe(-1); +}); diff --git a/tests/boundary/providers/ghidra/ghidraClient.test.ts b/tests/boundary/providers/ghidra/ghidraClient.test.ts index fa77d8c381..45dedebd00 100644 --- a/tests/boundary/providers/ghidra/ghidraClient.test.ts +++ b/tests/boundary/providers/ghidra/ghidraClient.test.ts @@ -36,6 +36,7 @@ const HOST_TRANSPORT: GhidraTransportKind = type FixtureMode = | "success" | "fragmented" + | "oversized_result" | "wrong_identity" | "malformed" | "contradictory" @@ -188,6 +189,32 @@ describe("GhidraClient", () => { 108, ); else expect(launcher.endpointPaths[0]).toMatch(/bridge-endpoint\.json$/u); + + await expect( + client.callTool("list_strings", { document: null }), + ).resolves.toMatchObject({ + ok: true, + value: [{ address: "0x2000", value: "fixture 雪🦊" }], + }); + }); + + it("accepts an owned-socket result above the former 64 MiB ceiling", async () => { + const client = clientFor(new FixtureLauncher("oversized_result")); + const result = await client.callTool("list_strings", { document: null }); + + expect(result.ok).toBe(true); + if (!result.ok) return; + if (!Array.isArray(result.value)) + throw new TypeError("Ghidra fixture result was not an inventory array"); + const first = result.value[0]; + if (typeof first !== "object" || first === null || Array.isArray(first)) + throw new TypeError("Ghidra fixture inventory item was not an object"); + const value = first.value; + if (typeof value !== "string") + throw new TypeError("Ghidra fixture string value was missing"); + expect(value.length).toBe(64 * 1024 * 1024 + 1); + expect(value.slice(0, 4)).toBe("xxxx"); + expect(value.slice(-4)).toBe("xxxx"); }); it("completes the same authenticated handshake over loopback TCP", async () => { diff --git a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts index 70bce00786..e263c6861b 100644 --- a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts @@ -79,6 +79,48 @@ describe("HopperClient protocol", () => { expect(results).toHaveLength(HOPPER_OPERATIONS.length); expect(results.every((result) => result.ok)).toBe(true); }); +}); + +describe("HopperClient response lifecycle", () => { + it("reassembles a fragmented Unicode reply from the owned socket", async () => { + const client = await startClient(); + + await expect( + client.callTool("echo", { label: "雪🦊", fragmented: true }), + ).resolves.toEqual({ + ok: true, + value: { label: "雪🦊", fragmented: true }, + }); + }); + + it("accepts an owned-socket response above the former 10 MiB ceiling", async () => { + const client = await startClient(); + const label = "x".repeat(10 * 1024 * 1024 + 1); + + const result = await client.callTool("echo", { label }); + + expect(result).toEqual({ ok: true, value: { label } }); + }); + + it("clears a partial reply when closing and restarting the owned session", async () => { + const launcher = new FixtureLauncher(); + const client = await startClient(launcher); + const partialReply = client.callTool("partial_then_wait", {}); + const request = await launcher.waitForRequest("partial_then_wait"); + const partialWrite = await launcher.waitForRequest("fixture_partial_write"); + expect(partialWrite.id).toBe(request.id); + + await client.close(); + await expect(partialReply).resolves.toMatchObject({ + ok: false, + error: { _tag: "HopperProcessError" }, + }); + await expect(client.start()).resolves.toMatchObject({ ok: true }); + await expect(client.callTool("echo", { label: "fresh" })).resolves.toEqual({ + ok: true, + value: { label: "fresh" }, + }); + }); it("rejects a bridge session with the wrong capability token", async () => { const client = new HopperClient({ diff --git a/tests/boundary/providers/hopper/linuxPrivateDisplay.test.ts b/tests/boundary/providers/hopper/linuxPrivateDisplay.test.ts index b4e378f821..9310d54639 100644 --- a/tests/boundary/providers/hopper/linuxPrivateDisplay.test.ts +++ b/tests/boundary/providers/hopper/linuxPrivateDisplay.test.ts @@ -25,6 +25,12 @@ const helperPath = fileURLToPath( const hangingHelperPath = fileURLToPath( new URL("../../../fixtures/x11ProbeHang.py", import.meta.url), ); +const dialogDetectionFixturePath = fileURLToPath( + new URL("../../../fixtures/hopperDialogDetection.py", import.meta.url), +); +const cleanupTimeoutFixturePath = fileURLToPath( + new URL("../../../fixtures/hopperCleanupTimeout.py", import.meta.url), +); type DiagnosticOverrides = Partial< Omit @@ -81,6 +87,46 @@ const failureExit = (code: HopperStartupFailureCode): number => ? 79 : 70; +describe("Hopper demo dialog detection", () => { + it.skipIf(process.platform !== "linux")( + "uses the Hopper transient-parent relation instead of exact dialog geometry", + () => { + const result = spawnSync( + "/usr/bin/python3", + [dialogDetectionFixturePath, helperPath], + { encoding: "utf8" }, + ); + expect(result.status).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ + dialog: [10, 20, 100, 1200, 284], + click: [136, 362], + wrong_parent_rejected: true, + }); + }, + ); +}); + +describe("Hopper process cleanup", () => { + it.skipIf(process.platform !== "linux")( + "kills a child that ignores SIGTERM after the bounded timeout", + () => { + const result = spawnSync( + "/usr/bin/python3", + [cleanupTimeoutFixturePath, helperPath], + { encoding: "utf8", timeout: 5_000 }, + ); + expect(result.status).toBe(0); + const parsed = JSON.parse(result.stdout) as { + readonly returncode: number; + readonly elapsed_ms: number; + }; + expect(parsed.returncode).toBe(-9); + expect(parsed.elapsed_ms).toBeGreaterThanOrEqual(1_800); + expect(parsed.elapsed_ms).toBeLessThan(4_000); + }, + ); +}); + describe("Linux private display selection", () => { it("prefers a successful direct Xvfb probe", async () => { const calls: LinuxPrivateDisplayRunnableStrategy[] = []; diff --git a/tests/boundary/providers/managed/managedArmMachine.test.ts b/tests/boundary/providers/managed/managedArmMachine.test.ts index 0436c631d3..7680423b8e 100644 --- a/tests/boundary/providers/managed/managedArmMachine.test.ts +++ b/tests/boundary/providers/managed/managedArmMachine.test.ts @@ -3,7 +3,6 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; -import { parseMachineType } from "../../../../src/domain/peInspection.js"; import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js"; import { ManagedStaticProvider } from "../../../../src/dotnet/ManagedStaticProvider.js"; @@ -24,7 +23,6 @@ it.each([0x01c0, 0x01c2, 0x01c4])( architecture: "arm", managed: true, }); - expect(parseMachineType(machine)).toBe("arm"); const client = new ManagedStaticProvider().createClient(parsed.value); try { for (const operation of [ diff --git a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts index f58c5032be..c6e187acb6 100644 --- a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts +++ b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts @@ -58,26 +58,6 @@ describe("artifact archive inventory", () => { expect(inspectionEvidence.evidence_links).toEqual( inspection.evidence_links, ); - const expandedInspection = artifactInspectionResultSchema.parse({ - ...inspection, - subject: { ...inspection.subject, root_format: "f".repeat(129) }, - observations: inspection.observations.map((observation) => ({ - ...observation, - subject: "s".repeat(4_097), - })), - hypotheses: inspection.hypotheses.map((hypothesis) => ({ - ...hypothesis, - statement: "s".repeat(4_097), - basis_evidence_ids: Array(9).fill(inspection.evidence_links[0] ?? ""), - })), - next_probes: inspection.next_probes.map((probe) => ({ - ...probe, - operation: "o".repeat(129), - })), - limitations: Array.from({ length: 1_001 }, (_, index) => `gap-${index}`), - }); - expect(expandedInspection.subject.root_format).toHaveLength(129); - expect(expandedInspection.limitations).toHaveLength(1_001); const cancellation = new AbortController(); cancellation.abort(); await expect( diff --git a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts index 98ab358089..3a3e78d436 100644 --- a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts +++ b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts @@ -80,13 +80,6 @@ describe("artifact archive safety", () => { ); it("rejects unsafe paths and collisions", async () => { - expect( - normalizeArtifactPath( - Array.from({ length: 24 }, (_, index) => `level${String(index)}`).join( - "/", - ) + "/entry.js", - ).split("/"), - ).toHaveLength(25); let unsafePathError: unknown; try { normalizeArtifactPath("../escape"); diff --git a/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts b/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts new file mode 100644 index 0000000000..25264cfa16 --- /dev/null +++ b/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts @@ -0,0 +1,57 @@ +import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; +import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js"; +import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +describe("artifact directory diagnostics", () => { + it.skipIf(process.platform === "win32")( + "preserves the inaccessible member path and errno through provider projection", + async () => { + const root = await createTestTempDirectory("rea-directory-eacces-"); + const appPath = join(root, "Denied.app"); + const contentsPath = join(appPath, "Contents"); + const executablePath = join(contentsPath, "MacOS", "Fixture"); + const blockedPath = join(contentsPath, "blocked.txt"); + await mkdir(join(contentsPath, "MacOS"), { recursive: true }); + await writeFile( + join(contentsPath, "Info.plist"), + "CFBundleExecutableFixture", + ); + const machHeader = Buffer.alloc(8); + machHeader.writeUInt32BE(0xfeedfacf, 0); + machHeader.writeUInt32BE(0x0100000c, 4); + await writeFile(executablePath, machHeader); + await writeFile(blockedPath, "member contents"); + await chmod(blockedPath, 0); + try { + const target = await parseBinaryTarget(appPath); + expect(target.ok).toBe(true); + if (!target.ok) return; + const result = await new ArtifactProvider() + .createClient(target.value) + .execute("inventory_artifact", {}); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "artifact_operation_failed", + details: { + operation: "inventory_artifact", + reason: "io", + }, + }); + const projected = projectAnalysisError(result.error); + const detail = projected.details?.detail; + expect(typeof detail).toBe("string"); + expect(detail).toContain("Denied.app/Contents/blocked.txt"); + expect(detail).toContain("EACCES"); + } finally { + await chmod(blockedPath, 0o600); + } + }, + ); +}); diff --git a/tests/boundary/providers/native/nativeCallObservation.test.ts b/tests/boundary/providers/native/nativeCallObservation.test.ts new file mode 100644 index 0000000000..4192a5276b --- /dev/null +++ b/tests/boundary/providers/native/nativeCallObservation.test.ts @@ -0,0 +1,293 @@ +import { createHash } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + AnalysisCapabilityUnavailableError, + AnalysisCancelledError, + AnalysisInputError, +} from "../../../../src/domain/analysisErrorCore.js"; +import type { AnalysisError } from "../../../../src/domain/analysisErrorBase.js"; +import { EvidenceIntegrityError } from "../../../../src/domain/evidenceErrors.js"; +import { nativeCallObservationResultSchema } from "../../../../src/domain/native/nativeCallObservation.js"; +import { err, ok, type Result } from "../../../../src/domain/result.js"; +import type { + NativeCallTrace, + NativeCallTracer, +} from "../../../../src/native/LldbCallTracer.js"; +import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; +import { + NativeFixtureRunner, + nativeMachoTarget, +} from "../../../fixtures/nativeCommands.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +const LOCATION = { + load_address: "0x100000928", + file_address: "0x100000928", + module: "Fixture", + module_path: "/tmp/Fixture", + symbol: "-[Greeter greet:times:]", +}; + +const run = ( + overrides: Partial = {}, +): NativeCallTrace["run"] => ({ + status: "traced", + version: "lldb-2100.0.17.203", + pid: 4242, + outcome: "exited", + exit_status: 0, + exit_description: null, + killed: false, + terminated: true, + elapsed_ms: 12.5, + breakpoints: [ + { index: 0, location_count: 1, locations: [LOCATION] }, + { index: 1, location_count: 1, locations: [LOCATION] }, + ], + events: [ + { + sequence: 0, + elapsed_ms: 3, + thread_id: 7, + breakpoint_index: 0, + ...LOCATION, + receiver_class: "Greeter", + selector: "greet:times:", + registers: [ + { name: "x0", value: "0x6000" }, + { name: "x1", value: "0x7000" }, + ], + backtrace: [], + }, + ], + other_stops: [], + ...overrides, +}); + +/** Records the request and returns a canned run or failure. */ +class FixtureTracer implements NativeCallTracer { + readonly requests: Parameters[0][] = []; + + constructor( + private readonly outcome: Result = ok({ + debugger: { + path: "/usr/bin/lldb", + sha256: "a".repeat(64), + version: "lldb-2100.0.17.203", + }, + run: run(), + stdout: { text: "hello\n", bytes: 6, truncated: false }, + stderr: { text: "", bytes: 0, truncated: false }, + terminated: true, + }), + ) {} + + trace(request: Parameters[0]) { + this.requests.push(request); + return Promise.resolve(this.outcome); + } +} + +const BREAKPOINTS = [ + { kind: "objc-method", class_name: "Greeter", selector: "greet:times:" }, + { kind: "function", name: "rea_add" }, +]; + +const fixtureTarget = async () => { + const directory = await createTestTempDirectory("rea-native-calls-"); + const path = join(directory, "Fixture"); + await writeFile(path, "fixture executable"); + return { + ...nativeMachoTarget(path), + sha256: createHash("sha256").update("fixture executable").digest("hex"), + }; +}; + +const observe = async ( + tracer: NativeCallTracer, + parameters: Record, + target?: Awaited>, + signal?: AbortSignal, +) => + new NativeMacOSProvider(new NativeFixtureRunner(), "darwin", tracer) + .createClient(target ?? (await fixtureTarget())) + .execute( + "observe_native_calls", + // The provider parses JSON-like parameters itself. + JSON.parse(JSON.stringify(parameters)), + signal === undefined ? undefined : { signal }, + ); + +describe("observe_native_calls projection", () => { + it("launches the active Mach-O through the tracer and projects its run", async () => { + const tracer = new FixtureTracer(); + const target = await fixtureTarget(); + const observed = await observe( + tracer, + { breakpoints: BREAKPOINTS, arguments: ["--flag"] }, + target, + ); + if (!observed.ok) throw observed.error; + expect(tracer.requests[0]).toMatchObject({ + executable: target.path, + architecture: "arm64", + input: { + arguments: ["--flag"], + duration_ms: 10_000, + max_events: 1_000, + argument_registers: 4, + backtrace_frames: 0, + }, + }); + const result = nativeCallObservationResultSchema.parse( + observed.value.result, + ); + expect(result.breakpoints.map(({ request }) => request)).toEqual([ + { + kind: "objc-method", + class_name: "Greeter", + selector: "greet:times:", + method_type: "any", + }, + { kind: "function", name: "rea_add", module: null }, + ]); + expect(result.process).toMatchObject({ + pid: 4242, + outcome: "exited", + exit_status: 0, + terminated: true, + stdout: { text: "hello\n" }, + }); + expect(result.events[0]).toMatchObject({ + receiver_class: "Greeter", + selector: "greet:times:", + }); + expect(result.coverage).toEqual({ + status: "complete", + event_limit_reached: false, + unresolved_breakpoints: [], + }); + expect(observed.value.locations).toEqual([ + { kind: "artifact-path", path: target.path }, + ]); + }); + + it("marks event limits, unresolved breakpoints and unconfirmed exits", async () => { + const tracer = new FixtureTracer( + ok({ + debugger: { + path: "/usr/bin/lldb", + sha256: "a".repeat(64), + version: null, + }, + run: run({ + outcome: "event-limit", + exit_status: null, + killed: true, + terminated: false, + breakpoints: [ + { index: 0, location_count: 1, locations: [LOCATION] }, + { index: 1, location_count: 0, locations: [] }, + ], + }), + stdout: { text: "", bytes: 0, truncated: false }, + stderr: { text: "x".repeat(16), bytes: 2_000_000, truncated: true }, + terminated: true, + }), + ); + const observed = await observe(tracer, { + breakpoints: BREAKPOINTS, + max_events: 1, + }); + if (!observed.ok) throw observed.error; + const result = nativeCallObservationResultSchema.parse( + observed.value.result, + ); + expect(result.coverage).toEqual({ + status: "partial", + event_limit_reached: true, + unresolved_breakpoints: [1], + }); + expect(result.process.terminated).toBe(false); + expect(result.limitations).toEqual( + expect.arrayContaining([ + "Observation stopped at max_events (1); later calls were not recorded and the process was killed.", + "Breakpoints 1 matched no code in any image loaded while observing; their calls could not be observed.", + "Target output beyond 1 MiB per stream is counted but not kept.", + "REA could not confirm that process 4242 exited; check for it before relying on host state.", + ]), + ); + }); +}); + +describe("observe_native_calls failures", () => { + it("rejects malformed input before launching anything", async () => { + for (const parameters of [ + { breakpoints: [] }, + { breakpoints: [{ kind: "objc-method", selector: "-[A b]" }] }, + { breakpoints: BREAKPOINTS, environment: { "A=B": "x" } }, + { breakpoints: BREAKPOINTS, argument_registers: 9 }, + ]) { + const tracer = new FixtureTracer(); + const observed = await observe(tracer, parameters); + expect(observed.ok).toBe(false); + if (!observed.ok) + expect(observed.error).toBeInstanceOf(AnalysisInputError); + expect(tracer.requests).toEqual([]); + } + }); + + it("refuses a changed or non-Mach-O target", async () => { + const tracer = new FixtureTracer(); + const target = await fixtureTarget(); + const changed = await observe( + tracer, + { breakpoints: BREAKPOINTS }, + { ...target, sha256: "f".repeat(64) }, + ); + expect(!changed.ok && changed.error).toBeInstanceOf(EvidenceIntegrityError); + const database = await new NativeMacOSProvider( + new NativeFixtureRunner(), + "darwin", + tracer, + ) + .createClient({ + path: target.path, + sha256: target.sha256, + kind: "database", + format: "analysis-database", + }) + .execute("observe_native_calls", { breakpoints: [] }); + expect(!database.ok && database.error).toBeInstanceOf( + AnalysisCapabilityUnavailableError, + ); + expect(tracer.requests).toEqual([]); + }); + + it("keeps the tracer's failure reason and caller cancellation", async () => { + const denied = new AnalysisCapabilityUnavailableError( + "native-macos", + "observe_native_calls", + "debugger-attach-denied: attach failed", + ); + const failed = await observe(new FixtureTracer(err(denied)), { + breakpoints: BREAKPOINTS, + }); + expect(!failed.ok && failed.error).toBe(denied); + const controller = new AbortController(); + controller.abort(); + const cancelled = await observe( + new FixtureTracer(), + { breakpoints: BREAKPOINTS }, + undefined, + controller.signal, + ); + expect(!cancelled.ok && cancelled.error).toBeInstanceOf( + AnalysisCancelledError, + ); + }); +}); diff --git a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts index b04633b966..2b949758df 100644 --- a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts +++ b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts @@ -1,9 +1,12 @@ -import { mkdir, realpath, writeFile } from "node:fs/promises"; +import { chmod, mkdir, readFile, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { build } from "plist"; import { describe, expect, it } from "vitest"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; +import { execFileOutput } from "../../../../src/process/ExecFileOutput.js"; +import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js"; import { NativeDmgArtifactReader, type NativeDmgHost, @@ -53,9 +56,19 @@ describe("native DMG artifact reader", () => { it("rejects non-zero results and surfaces detach failure during attach cleanup", async () => { await expect( NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { - run: () => Promise.resolve({ stdout: "", exitCode: 1 }), + run: () => + Promise.resolve({ + stdout: "verify output", + stderr: "hdiutil: verify failed - image not recognized", + exitCode: 1, + }), }), - ).rejects.toThrow("non-zero exit code"); + ).rejects.toMatchObject({ + reason: "format", + message: expect.stringMatching( + /"arguments":\["verify","\/tmp\/image\.dmg"\].*"exitCode":1.*"stderr":"hdiutil: verify failed - image not recognized"/u, + ), + }); const host: NativeDmgHost = { run(arguments_) { @@ -91,6 +104,238 @@ describe("native DMG artifact reader", () => { }); }); +describe("native DMG command failure diagnostics", () => { + it("keeps unknown attach failures unavailable with command evidence", async () => { + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run(arguments_) { + if (arguments_[0] === "verify") + return Promise.resolve({ stdout: "", exitCode: 0 }); + return Promise.resolve({ + stdout: "attach output", + stderr: "attach failed for an unknown reason", + exitCode: 2, + }); + }, + }), + ).rejects.toMatchObject({ + reason: "unavailable", + message: expect.stringMatching( + /"arguments":\["attach".*"exitCode":2.*"stderr":"attach failed for an unknown reason"/u, + ), + }); + }); + + it("keeps unrecognized numeric verify failures unknown and maps the observed I/O diagnostic", async () => { + const unknown = await NativeDmgArtifactReader.create( + "/tmp/image.dmg", + undefined, + { + run: () => + Promise.resolve({ + stdout: "verify stdout", + stderr: "hdiutil: verify failed - an unrecognized system failure", + exitCode: 1, + }), + }, + ).catch((cause: unknown) => cause); + expect(unknown).toBeInstanceOf(ArtifactReaderFailure); + if (!(unknown instanceof ArtifactReaderFailure)) return; + expect(unknown.reason).toBe("unavailable"); + expect(unknown.message).toContain("do not establish the failure cause"); + expect(unknown.message).toContain('"exitCode":1'); + expect(unknown.message).toContain('"stdout":"verify stdout"'); + expect(unknown.message).toContain("an unrecognized system failure"); + + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.resolve({ + stdout: "", + stderr: "hdiutil: verify failed - Input/output error\n", + exitCode: 1, + }), + }), + ).rejects.toMatchObject({ + reason: "io", + message: expect.stringContaining("Input/output error"), + }); + + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.resolve({ + stdout: "checksum output", + stderr: "hdiutil: verify failed - image data corrupted\n", + exitCode: 1, + }), + }), + ).rejects.toMatchObject({ + reason: "integrity", + message: expect.stringContaining("image data corrupted"), + }); + }); + + it("distinguishes child launch and host permission failures", async () => { + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.reject( + Object.assign(new Error("could not launch hdiutil"), { + code: "ENOENT", + syscall: "spawn /usr/bin/hdiutil", + }), + ), + }), + ).rejects.toMatchObject({ + reason: "unavailable", + message: expect.stringContaining('"code":"ENOENT"'), + }); + + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.reject( + Object.assign(new Error("permission denied"), { + code: "EACCES", + syscall: "spawn /usr/bin/hdiutil", + }), + ), + }), + ).rejects.toMatchObject({ + reason: "io", + message: expect.stringContaining('"code":"EACCES"'), + }); + }); +}); + +describe("native DMG real verification diagnostics", () => { + it.skipIf(process.platform !== "darwin")( + "classifies a real hdiutil verify refusal as a format failure with captured output", + async () => { + const root = await createTestTempDirectory("rea-invalid-dmg-"); + const path = join(root, "invalid.dmg"); + await writeFile(path, "This is not a disk image.\n"); + const failure = await NativeDmgArtifactReader.create(path).catch( + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) return; + expect(failure.reason).toBe("format"); + expect(failure.message).toContain('"command":"/usr/bin/hdiutil"'); + expect(failure.message).toContain('"arguments":["verify"'); + expect(failure.message).toContain('"exitCode":1'); + expect(failure.message).toContain('"stdout":""'); + expect(failure.message).toContain('"stderr":"'); + }, + ); + + it.skipIf(process.platform !== "darwin")( + "classifies real hdiutil target access failures as I/O, separately from malformed images", + async () => { + const root = await createTestTempDirectory("rea-dmg-target-errors-"); + const missingPath = join(root, "missing.dmg"); + const unreadablePath = join(root, "unreadable.dmg"); + await writeFile(unreadablePath, "not a disk image\n"); + await chmod(unreadablePath, 0); + try { + for (const [path, expectedDiagnostic] of [ + [missingPath, "No such file or directory"], + [unreadablePath, "Permission denied"], + ] as const) { + const failure = await NativeDmgArtifactReader.create(path).catch( + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) continue; + expect(failure.reason).toBe("io"); + expect(failure.message).toContain('"command":"/usr/bin/hdiutil"'); + expect(failure.message).toContain('"arguments":["verify"'); + expect(failure.message).toContain('"exitCode":1'); + expect(failure.message).toContain('"stdout":""'); + expect(failure.message).toContain(expectedDiagnostic); + expect(failure.cause).toBeInstanceOf(Error); + if (!(failure.cause instanceof Error)) continue; + expect(Reflect.get(failure.cause, "code")).toBe(1); + expect(Reflect.get(failure.cause, "stderr")).toContain( + expectedDiagnostic, + ); + } + } finally { + await chmod(unreadablePath, 0o600); + } + }, + ); + + it.skipIf(process.platform !== "darwin")( + "classifies a checksum mismatch in an hdiutil-created DMG as an integrity failure", + async () => { + const root = await createTestTempDirectory( + "rea-dmg-checksum-corruption-", + ); + const writableImagePath = join(root, "writable.dmg"); + const imagePath = join(root, "fixture.dmg"); + await execFileOutput("/usr/bin/hdiutil", [ + "create", + "-quiet", + "-size", + "1m", + "-layout", + "NONE", + "-type", + "UDIF", + writableImagePath, + ]); + await execFileOutput("/usr/bin/hdiutil", [ + "convert", + "-quiet", + writableImagePath, + "-format", + "UDRO", + "-o", + imagePath, + ]); + const valid = await execFileOutput("/usr/bin/hdiutil", [ + "verify", + imagePath, + ]); + expect(valid.stderr).toContain("is VALID"); + + const image = await readFile(imagePath); + const footer = image.subarray(-512); + expect(footer.subarray(0, 4).toString("ascii")).toBe("koly"); + const dataForkOffset = Number(footer.readBigUInt64BE(24)); + const dataForkLength = Number(footer.readBigUInt64BE(32)); + expect(dataForkLength).toBeGreaterThan(4096); + const corruptionOffset = dataForkOffset + 4096; + expect(corruptionOffset).toBeLessThan(dataForkOffset + dataForkLength); + const originalByte = image[corruptionOffset]; + if (originalByte === undefined) + throw new Error("expected a byte in the generated image data fork"); + image[corruptionOffset] = originalByte ^ 0x01; + await writeFile(imagePath, image); + + const failure = await NativeDmgArtifactReader.create(imagePath).catch( + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) return; + expect(failure.reason).toBe("integrity"); + expect(failure.message).toContain('"command":"/usr/bin/hdiutil"'); + expect(failure.message).toContain('"arguments":["verify"'); + expect(failure.message).toContain('"exitCode":1'); + expect(failure.message).toContain("calculated CRC32"); + expect(failure.message).toContain("expected CRC32"); + expect(failure.message).toContain("verify failed - invalid checksum"); + expect(failure.cause).toBeInstanceOf(Error); + if (!(failure.cause instanceof Error)) return; + expect(Reflect.get(failure.cause, "code")).toBe(1); + expect(Reflect.get(failure.cause, "stderr")).toContain("is INVALID"); + }, + 60_000, + ); +}); + it("owns the canonical mount root and detaches each observed whole image once", async () => { const calls: string[][] = []; const host: NativeDmgHost = { diff --git a/tests/boundary/providers/native/nativeNumericPermissions.test.ts b/tests/boundary/providers/native/nativeNumericPermissions.test.ts deleted file mode 100644 index 3ca85ec75a..0000000000 --- a/tests/boundary/providers/native/nativeNumericPermissions.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, it } from "vitest"; -import { parseOtoolLoadCommands } from "../../../../src/native/parsers/otool.js"; - -it.each([0, 1, 2, 3, 4, 5, 6, 7])( - "decodes Mach VM protection bits for mask %i", - (mask) => { - const parsed = parseOtoolLoadCommands(`Load command 0 - cmd LC_SEGMENT_64 - segname __DATA - maxprot 0x${mask.toString(16)} - initprot ${mask} -`); - for (const permissions of [ - parsed.segments[0]?.maximum_permissions, - parsed.segments[0]?.initial_permissions, - ]) { - expect(permissions).toMatchObject({ - read: (mask & 1) !== 0, - write: (mask & 2) !== 0, - execute: (mask & 4) !== 0, - }); - } - }, -); diff --git a/tests/composition/analysis-sessions/artifactComparison.test.ts b/tests/composition/analysis-sessions/artifactComparison.test.ts index 36fbfaf527..a3850df20b 100644 --- a/tests/composition/analysis-sessions/artifactComparison.test.ts +++ b/tests/composition/analysis-sessions/artifactComparison.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; import { fragmentInventoryEvidence } from "../../fixtures/artifactEvidence.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; import { artifactComparisonResultSchema, diff --git a/tests/composition/analysis-sessions/binarySession.test.ts b/tests/composition/analysis-sessions/binarySession.test.ts index da99e17e80..205692e773 100644 --- a/tests/composition/analysis-sessions/binarySession.test.ts +++ b/tests/composition/analysis-sessions/binarySession.test.ts @@ -3,6 +3,7 @@ import type { AnalysisProvider, } from "../../../src/application/AnalysisProvider.js"; import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; +import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; import type { RecordUnknownInput } from "../../../src/domain/residualUnknown.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; @@ -10,6 +11,8 @@ import { HopperStartError } from "../../../src/domain/hopperErrors.js"; import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { err, ok as resultOk } from "../../../src/domain/result.js"; +import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; +import { createEvidence } from "../../../src/domain/evidence.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; import { ControllableAnalysisClient, @@ -157,6 +160,65 @@ describe("detached provider and target metadata", () => { }); }); +describe("opening previewed targets", () => { + it("rechecks the active target after another serialized transition", async () => { + const [first, second] = await createBinarySessionTargets(); + const calls: string[] = []; + const session = createTestBinarySession(createCacheProvider(calls)); + try { + expect((await session.open(first)).ok).toBe(true); + const target = session.activeTarget(); + if (target === undefined) throw new Error("Expected an active target"); + const preview = await session.previewTarget(target); + if (!preview.ok) throw preview.error; + expect(preview.value.sameTarget).toBe(true); + expect((await session.open(second)).ok).toBe(true); + expect((await session.openResolvedTarget(preview.value)).ok).toBe(true); + expect(session.activeTarget()?.path).toBe(first); + expect(calls).toEqual(["health", "health", "health"]); + } finally { + await session.close(); + } + }); + + it("rejects cancellation and a staged snapshot changed after preview without startup", async () => { + const [first, second] = await createBinarySessionTargets(); + const calls: string[] = []; + const session = createTestBinarySession(createCacheProvider(calls)); + const other = createTestBinarySession(createCacheProvider([])); + try { + const parsed = await parseBinaryTarget(first); + if (!parsed.ok) throw parsed.error; + const preview = await session.previewTarget(parsed.value); + if (!preview.ok) throw preview.error; + const controller = new AbortController(); + controller.abort(); + const cancelled = await session.openResolvedTarget(preview.value, { + signal: controller.signal, + }); + expect(cancelled).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + + expect((await other.open(second)).ok).toBe(true); + const snapshot = other.exportAnalysisSnapshot(); + if (!snapshot.ok) throw snapshot.error; + expect(session.importAnalysisSnapshot(snapshot.value).ok).toBe(true); + const rejected = await session.openResolvedTarget(preview.value); + expect(rejected).toMatchObject({ + ok: false, + error: { _tag: "EvidenceIntegrityError" }, + }); + expect(calls).toEqual([]); + expect(session.activeTarget()).toBeUndefined(); + } finally { + await session.close(); + await other.close(); + } + }); +}); + describe("non-hopper provider dispatch", () => { it("runs through a non-Hopper analysis provider", async () => { const [first] = await createBinarySessionTargets(); @@ -267,6 +329,144 @@ describe("fresh run identity", () => { }); }); +describe("evidence metadata imports and snapshot cache", () => { + it("retains cached queries and exports after additive evidence imports", async () => { + const [target] = await createBinarySessionTargets(); + const calls: string[] = []; + const session = createTestBinarySession(createCacheProvider(calls)); + expect((await session.open(target)).ok).toBe(true); + expect( + ( + await session.execute("address_name", { + address: "0x1000", + document: "first", + }) + ).ok, + ).toBe(true); + const before = session.exportAnalysisSnapshot(); + expect(before.ok).toBe(true); + if (!before.ok) { + await session.close(); + return; + } + const existing = session.exportEvidenceBundle().records[0]; + expect(existing?.subject).not.toBeNull(); + if (existing?.subject === null || existing === undefined) { + await session.close(); + return; + } + const addition = createEvidence( + { + path: existing.subject.local_path, + sha256: existing.subject.digest.sha256, + format: existing.subject.format, + ...(existing.subject.architecture === null + ? {} + : { architecture: existing.subject.architecture }), + }, + { id: "fixture", name: "Fixture", version: "1" }, + { + predicateType: "rea.analysis", + operation: "health", + parameters: { source: "additive-import" }, + result: true, + }, + ); + expect( + session.importEvidenceBundle(createEvidenceBundle([addition])), + ).toEqual({ ok: true, value: 1 }); + expect(session.exportAnalysisSnapshot()).toMatchObject({ + ok: true, + value: { + entries: before.value.entries, + evidence_bundle: { records: expect.arrayContaining([addition]) }, + }, + }); + const callsAfterInitialRead = [...calls]; + expect( + ( + await session.execute("address_name", { + address: "0x1000", + document: "first", + }) + ).ok, + ).toBe(true); + expect(calls).toEqual(callsAfterInitialRead); + expect(session.exportAnalysisSnapshot().ok).toBe(true); + await session.close(); + }); + + it("invalidates cached analysis when an import changes evidence path metadata", async () => { + const [target] = await createBinarySessionTargets(); + const calls: string[] = []; + const session = createTestBinarySession(createCacheProvider(calls)); + expect((await session.open(target)).ok).toBe(true); + expect( + ( + await session.execute("address_name", { + address: "0x1000", + document: "first", + }) + ).ok, + ).toBe(true); + expect(session.exportAnalysisSnapshot().ok).toBe(true); + + const evidence = session + .exportEvidenceBundle() + .records.find(({ operation }) => operation === "address_name"); + expect(evidence?.subject).not.toBeNull(); + if (evidence?.subject === null || evidence === undefined) { + await session.close(); + return; + } + expect( + session.importEvidenceBundle(createEvidenceBundle([evidence])), + ).toEqual({ ok: true, value: 0 }); + expect(session.exportAnalysisSnapshot().ok).toBe(true); + const callsAfterInitialRead = [...calls]; + expect( + ( + await session.execute("address_name", { + address: "0x1000", + document: "first", + }) + ).ok, + ).toBe(true); + expect(calls).toEqual(callsAfterInitialRead); + + const relocated = { + ...evidence, + subject: { + ...evidence.subject, + local_path: `${evidence.subject.local_path}.moved`, + name: `${evidence.subject.name}.moved`, + }, + }; + expect( + session.importEvidenceBundle(createEvidenceBundle([relocated])), + ).toEqual({ ok: true, value: 0 }); + expect(session.exportEvidenceBundle().records).toContainEqual(relocated); + expect( + ( + await session.execute("address_name", { + address: "0x1000", + document: "first", + }) + ).ok, + ).toBe(true); + expect(calls).toHaveLength(callsAfterInitialRead.length + 1); + expect(session.exportAnalysisSnapshot()).toMatchObject({ + ok: false, + error: { + message: expect.stringContaining( + "Analysis snapshots are unavailable after analysis metadata mutations", + ), + }, + }); + await session.close(); + }); +}); + describe("replay of exact immutable calls", () => { it("replays exact immutable calls from a matching provider-neutral snapshot", async () => { const [first, second] = await createBinarySessionTargets(); diff --git a/tests/composition/android/application.test.ts b/tests/composition/android/application.test.ts index fe8def8ad8..ad7ff2b948 100644 --- a/tests/composition/android/application.test.ts +++ b/tests/composition/android/application.test.ts @@ -99,6 +99,50 @@ describe("Android application projection", () => { }); }); + it("keeps uppercase path-derived bytecode families distinct from byte validity", async () => { + const root = await createTestTempDirectory("rea-android-suffixes-"); + const path = join(root, "Suffixes.apk"); + const writer = new ZipWriter(new Uint8ArrayWriter()); + for (const entry of [ + "classes.DEX", + "extra.dex", + "Main.CLASS", + "Other.class", + ]) + await writer.add(entry, new TextReader("unrecognized bytes")); + await writeFile(path, await writer.close()); + + const inventory = parseEvidence( + await runProviderAnalysis(path, "inventory_artifact", {}), + ); + const result = projectAndroidApplicationEvidence({ + inventory_evidence: [inventory], + }); + const projection = androidApplicationProjectionResultSchema.parse( + requireSuccessfulProjection(result).normalized_result, + ); + + expect(projection.components.dex.map(({ path }) => path).sort()).toEqual([ + "classes.DEX", + "extra.dex", + ]); + expect( + projection.components.jvm_classes.map(({ path }) => path).sort(), + ).toEqual(["Main.CLASS", "Other.class"]); + expect( + projection.components.dex.every(({ format }) => format === "file"), + ).toBe(true); + expect( + projection.components.jvm_classes.every( + ({ format }) => format === "file", + ), + ).toBe(true); + expect(projection.runtime_families).toEqual(["dalvik-art", "java-kotlin"]); + expect(projection.limitations).toContain( + "Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid DEX or JVM class bytes.", + ); + }); + it("returns every component and bridge candidate from the inventory", async () => { const root = await createTestTempDirectory("rea-android-complete-"); const path = join(root, "Fixture.apk"); diff --git a/tests/composition/directAnalysisSnapshotReplay.test.ts b/tests/composition/directAnalysisSnapshotReplay.test.ts new file mode 100644 index 0000000000..9f5d09ce45 --- /dev/null +++ b/tests/composition/directAnalysisSnapshotReplay.test.ts @@ -0,0 +1,633 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { runDirectAnalysis } from "../../src/application/DirectAnalysis.js"; +import type { + AnalysisProvider, + CapabilityDescriptor, + ProviderIdentity, +} from "../../src/application/AnalysisProvider.js"; +import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js"; +import { createAnalysisProfile } from "../../src/domain/analysisProfile.js"; +import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js"; +import { ok } from "../../src/domain/result.js"; +import { + readAnalysisSnapshot, + writeAnalysisSnapshot, +} from "../../src/application/binary/AnalysisSnapshotFiles.js"; +import { createEvidence, type Evidence } from "../../src/domain/evidence.js"; +import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js"; +import { + REA_WORKFLOW_PROVIDER, + workflowAnalysisProfile, +} from "../../src/application/InvestigationProviders.js"; +import { + createAnalysisSnapshotWorkflowEntry, + type AnalysisSnapshot, +} from "../../src/domain/analysisSnapshot.js"; +import { createTestBinarySession } from "../fixtures/binarySession.js"; +import { BinarySession } from "../../src/application/binary/BinarySession.js"; +import { SessionProviderRouter } from "../../src/application/binary/SessionProviderRouter.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + +const IDENTITY = { + id: "snapshot-fixture", + name: "Snapshot Fixture Provider", + version: "1", +} as const; + +const operations = [ + "list_segments", + "list_documents", + "list_procedures", + "list_strings", +] as const; + +const workflowProviderOperations = [ + ...operations, + "analyze_function", + "list_names", + "search_strings", + "search_procedures", + "xrefs", + "resolve_containing_procedure", +] as const; + +const functionDossier = { + procedure: { + address: "0x1000", + name: "fixture", + signature: null, + locals: [], + }, + pseudocode: "return 0;", + assembly: ["ret"], + comments: [], + callers: [], + callees: [], + incoming_references: [], + outgoing_references: [], + referenced_strings: [], + referenced_names: [], + basic_blocks: [], + native_api: null, + native_value_flow: null, + limitations: [], +}; + +const makeProvider = ( + starts: string[], + calls: string[], + profile = createAnalysisProfile(IDENTITY, { fixture: true }), + identity: ProviderIdentity = profile.provider, + supportedOperations: readonly CapabilityDescriptor["operation"][] = operations, + resolutions: string[] = [], +): AnalysisProvider => { + const capabilities: CapabilityDescriptor[] = supportedOperations.map( + (operation) => ({ + operation, + provider: identity, + available: true, + reason: null, + cachePolicy: "snapshot", + effects: { + mutatesArtifact: false, + launchesProcess: true, + mayShowUi: false, + mayAccessNetwork: false, + mayWriteFilesystem: false, + changesPermissions: false, + requiresRoot: false, + }, + limitations: [], + }), + ); + return { + identity: () => identity, + capabilities: () => capabilities, + resolveAnalysisProfile: async () => { + resolutions.push(profile.digest); + return ok({ profile, compatibility: {} }); + }, + createClient: () => { + starts.push("start"); + return { + execute: async (operation) => { + calls.push(operation); + const results: Readonly> = { + list_segments: [{ name: "__TEXT", start: "0x1000", end: "0x2000" }], + list_documents: ["fixture"], + list_procedures: ["0x1000"], + list_strings: { "0x1000": "fixture" }, + analyze_function: functionDossier, + list_names: [{ address: "0x1000", name: "fixture" }], + search_strings: [{ address: "0x1000", value: "fixture" }], + search_procedures: [{ address: "0x1000", value: "fixture" }], + xrefs: [], + resolve_containing_procedure: null, + procedure_pseudo_code: "return 0;", + }; + const result = results[operation] ?? null; + return ok( + createAnalysisExecution(result, profile.provider, { + analysisProfile: profile, + rawResult: result, + }), + ); + }, + close: async () => undefined, + }; + }, + }; +}; + +const withAlternateWorkflowProfile = ( + snapshot: AnalysisSnapshot, + path: string, + current: Evidence, +): AnalysisSnapshot => { + const alternateProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, { + workflow: "binary_overview", + fixture: "alternate-profile", + }); + const subject = { + path, + sha256: snapshot.target.sha256, + format: snapshot.target.format, + ...(snapshot.target.architecture === null + ? {} + : { architecture: snapshot.target.architecture }), + }; + const alternateEvidence = createEvidence(subject, REA_WORKFLOW_PROVIDER, { + operation: current.operation, + parameters: current.parameters, + result: current.normalized_result, + rawResult: current.raw_result, + analysisProfile: alternateProfile, + confidence: "derived", + limitations: current.limitations, + locations: current.locations, + }); + const alternateEntry = createAnalysisSnapshotWorkflowEntry({ + target: snapshot.target, + binding: snapshot.binding, + operation: current.operation, + parameters: current.parameters, + execution: { + result: current.normalized_result, + rawResult: current.raw_result, + provider: REA_WORKFLOW_PROVIDER, + analysisProfile: alternateProfile, + limitations: current.limitations, + locations: current.locations, + subject, + }, + }); + return { + ...snapshot, + workflow_entries: [alternateEntry], + evidence_bundle: createEvidenceBundle([ + ...snapshot.evidence_bundle.records, + alternateEvidence, + ]), + }; +}; + +const withEarlierHistoricalEvidence = ( + snapshot: AnalysisSnapshot, + path: string, + current: Evidence, +): AnalysisSnapshot => { + const older = Array.from({ length: 64 }, (_, index) => + createEvidence( + { + path, + sha256: snapshot.target.sha256, + format: "analysis-database", + }, + REA_WORKFLOW_PROVIDER, + { + operation: "binary_overview", + parameters: {}, + result: `historical-${index}`, + analysisProfile: workflowAnalysisProfile( + snapshot.binding.analysis_profile, + ), + confidence: "derived", + limitations: ["Derived by an REA composed workflow."], + }, + ), + ).find((record) => record.evidence_id < current.evidence_id); + if (older === undefined) + throw new Error("fixture could not construct earlier historical Evidence"); + return { + ...snapshot, + evidence_bundle: createEvidenceBundle([ + ...snapshot.evidence_bundle.records, + older, + ]), + }; +}; + +describe("direct analysis composed snapshot replay", () => { + it.each([ + { + tool: "search_strings", + arguments: { pattern: "uncached", document: "fixture" }, + }, + { tool: "inspect_native_api", arguments: { procedure: "0x2000" } }, + ] as const)( + "reuses the previewed profile on a $tool snapshot miss", + async (scenario) => { + const directory = await createTestTempDirectory( + "rea-snapshot-miss-route-", + ); + const path = join(directory, "fixture.hop"); + const snapshotPath = join(directory, "snapshot.json"); + await writeFile(path, "fixture"); + const starts: string[] = []; + const calls: string[] = []; + const resolutions: string[] = []; + const profile = createAnalysisProfile(IDENTITY, { fixture: true }); + const provider = makeProvider( + starts, + calls, + profile, + IDENTITY, + workflowProviderOperations, + resolutions, + ); + const dependencies: DirectAnalysisDependencies = { + createBinarySession: () => createTestBinarySession(provider), + createManagedBinarySession: () => createTestBinarySession(provider), + }; + await runDirectAnalysis( + dependencies, + path, + "binary_overview", + {}, + { snapshotPath }, + ); + expect(resolutions).toHaveLength(1); + + const first = await runDirectAnalysis( + dependencies, + path, + scenario.tool, + scenario.arguments, + { snapshotPath }, + ); + expect(first).toMatchObject({ operation: scenario.tool }); + expect(resolutions).toHaveLength(2); + expect(starts).toHaveLength(2); + const callsAfterMiss = [...calls]; + + const replay = await runDirectAnalysis( + dependencies, + path, + scenario.tool, + scenario.arguments, + { snapshotPath }, + ); + expect(replay).toEqual(first); + expect(resolutions).toHaveLength(3); + expect(starts).toHaveLength(2); + expect(calls).toEqual(callsAfterMiss); + }, + ); + + for (const scenario of [ + { + tool: "inspect_native_api", + arguments: { procedure: "0x1000" }, + }, + { + tool: "inspect_native_dispatch_metadata", + arguments: {}, + }, + { + tool: "trace_feature", + arguments: { query: "fixture" }, + }, + { + tool: "trace_native_values", + arguments: { procedure: "0x1000" }, + }, + ] as const) { + it(`persists and replays ${scenario.tool} without provider startup`, async () => { + const directory = await createTestTempDirectory("rea-workflow-command-"); + const path = join(directory, "fixture.hop"); + const snapshotPath = join(directory, "snapshot.json"); + await writeFile(path, "fixture"); + const starts: string[] = []; + const calls: string[] = []; + const provider = makeProvider( + starts, + calls, + createAnalysisProfile(IDENTITY, { fixture: true }), + IDENTITY, + workflowProviderOperations, + ); + const dependencies: DirectAnalysisDependencies = { + createBinarySession: () => createTestBinarySession(provider), + createManagedBinarySession: () => createTestBinarySession(provider), + }; + const first = await runDirectAnalysis( + dependencies, + path, + scenario.tool, + scenario.arguments, + { snapshotPath }, + ); + const callsAfterFirst = [...calls]; + const startsAfterFirst = [...starts]; + const loaded = await readAnalysisSnapshot(snapshotPath); + if (!loaded.ok) throw loaded.error; + expect( + loaded.value.workflow_entries?.map(({ operation }) => operation), + ).toContain(scenario.tool); + + const second = await runDirectAnalysis( + dependencies, + path, + scenario.tool, + scenario.arguments, + { snapshotPath }, + ); + + expect(second).toEqual(first); + expect(calls).toEqual(callsAfterFirst); + expect(starts).toEqual(startsAfterFirst); + }); + } +}); + +describe("binary overview snapshot replay", () => { + it("replays the identical binary overview without provider startup or calls", async () => { + const directory = await createTestTempDirectory("rea-workflow-snapshot-"); + const path = join(directory, "fixture.hop"); + const snapshotPath = join(directory, "snapshot.json"); + await writeFile(path, "fixture"); + const starts: string[] = []; + const calls: string[] = []; + const provider = makeProvider(starts, calls); + const dependencies: DirectAnalysisDependencies = { + createBinarySession: () => createTestBinarySession(provider), + createManagedBinarySession: () => createTestBinarySession(provider), + }; + + const first = await runDirectAnalysis( + dependencies, + path, + "binary_overview", + {}, + { snapshotPath }, + ); + expect(calls).toEqual(["health", ...operations]); + expect(starts).toHaveLength(1); + + const loaded = await readAnalysisSnapshot(snapshotPath); + if (!loaded.ok) throw loaded.error; + expect(loaded.value.workflow_entries).toHaveLength(1); + const current = loaded.value.evidence_bundle.records.find( + (record) => record.operation === "binary_overview", + ); + if (current === undefined) + throw new Error("composed Evidence was not saved"); + expect( + ( + await writeAnalysisSnapshot( + withEarlierHistoricalEvidence(loaded.value, path, current), + snapshotPath, + true, + ) + ).ok, + ).toBe(true); + + const second = await runDirectAnalysis( + dependencies, + path, + "binary_overview", + {}, + { snapshotPath }, + ); + expect(second).toEqual(first); + expect(calls).toEqual(["health", ...operations]); + expect(starts).toHaveLength(1); + }); +}); + +describe("snapshot replay provider identity", () => { + it("uses the resolved provider version when capability metadata omits it", async () => { + const directory = await createTestTempDirectory("rea-profile-version-"); + const path = join(directory, "fixture.hop"); + const snapshotPath = join(directory, "snapshot.json"); + await writeFile(path, "fixture"); + const starts: string[] = []; + const calls: string[] = []; + const unresolvedIdentity = { ...IDENTITY, version: null }; + const resolvedProfile = createAnalysisProfile( + { ...IDENTITY, version: "launcher-sha256:fixture" }, + { fixture: true }, + ); + const provider = makeProvider( + starts, + calls, + resolvedProfile, + unresolvedIdentity, + ["search_strings"], + ); + const dependencies: DirectAnalysisDependencies = { + createBinarySession: () => createTestBinarySession(provider), + createManagedBinarySession: () => createTestBinarySession(provider), + }; + const first = await runDirectAnalysis( + dependencies, + path, + "search_strings", + { pattern: "fixture", document: "fixture" }, + { snapshotPath }, + ); + const callsAfterFirst = [...calls]; + const startsAfterFirst = [...starts]; + const firstSnapshot = await readAnalysisSnapshot(snapshotPath); + if (!firstSnapshot.ok) throw firstSnapshot.error; + expect(firstSnapshot.value.entries[0]?.execution.provider.version).toBe( + "launcher-sha256:fixture", + ); + + const second = await runDirectAnalysis( + dependencies, + path, + "search_strings", + { pattern: "fixture", document: "fixture" }, + { snapshotPath }, + ); + expect(calls).toEqual(callsAfterFirst); + expect(starts).toEqual(startsAfterFirst); + expect(second).toEqual(first); + + const changedProfileProvider = makeProvider( + starts, + calls, + createAnalysisProfile( + { ...IDENTITY, version: "launcher-sha256:changed" }, + { fixture: true }, + ), + unresolvedIdentity, + ["search_strings"], + ); + const changedProfileResult = await runDirectAnalysis( + { + createBinarySession: () => + createTestBinarySession(changedProfileProvider), + createManagedBinarySession: () => + createTestBinarySession(changedProfileProvider), + }, + path, + "search_strings", + { pattern: "fixture", document: "fixture" }, + { snapshotPath }, + ); + expect(changedProfileResult).not.toEqual(first); + expect(changedProfileResult).toMatchObject({ + code: "evidence_integrity_mismatch", + }); + expect(starts).toEqual(startsAfterFirst); + expect(calls).toEqual(callsAfterFirst); + + const changedProviderIdentity = { + id: "other-provider", + name: "Other Provider", + version: null, + }; + const changedProvider = makeProvider( + starts, + calls, + createAnalysisProfile( + { ...changedProviderIdentity, version: "other-version" }, + { fixture: true }, + ), + changedProviderIdentity, + ["search_strings"], + ); + const changedProviderResult = await runDirectAnalysis( + { + createBinarySession: () => createTestBinarySession(changedProvider), + createManagedBinarySession: () => + createTestBinarySession(changedProvider), + }, + path, + "search_strings", + { pattern: "fixture", document: "fixture" }, + { snapshotPath }, + ); + expect(changedProviderResult).not.toEqual(first); + expect(changedProviderResult).toMatchObject({ + code: "evidence_integrity_mismatch", + }); + expect(starts).toEqual(startsAfterFirst); + expect(calls).toEqual(callsAfterFirst); + }); +}); + +describe("workflow snapshot profile and cancellation binding", () => { + it("does not replay a valid entry from another workflow profile", async () => { + const directory = await createTestTempDirectory("rea-workflow-profile-"); + const path = join(directory, "fixture.hop"); + const snapshotPath = join(directory, "snapshot.json"); + await writeFile(path, "fixture"); + const starts: string[] = []; + const calls: string[] = []; + const provider = makeProvider(starts, calls); + const dependencies: DirectAnalysisDependencies = { + createBinarySession: () => createTestBinarySession(provider), + createManagedBinarySession: () => createTestBinarySession(provider), + }; + await runDirectAnalysis( + dependencies, + path, + "binary_overview", + {}, + { snapshotPath }, + ); + const loaded = await readAnalysisSnapshot(snapshotPath); + if (!loaded.ok) throw loaded.error; + const current = loaded.value.evidence_bundle.records.find( + (record) => record.operation === "binary_overview", + ); + if (current === undefined) + throw new Error("composed Evidence was not saved"); + expect( + ( + await writeAnalysisSnapshot( + withAlternateWorkflowProfile(loaded.value, path, current), + snapshotPath, + true, + ) + ).ok, + ).toBe(true); + + await runDirectAnalysis( + dependencies, + path, + "binary_overview", + {}, + { snapshotPath }, + ); + expect(calls).toEqual(["health", ...operations, "health", ...operations]); + expect(starts).toHaveLength(2); + }); + + it("does not return a cached result when cancelled during route resolution", async () => { + const directory = await createTestTempDirectory("rea-workflow-cancel-"); + const path = join(directory, "fixture.hop"); + const snapshotPath = join(directory, "snapshot.json"); + await writeFile(path, "fixture"); + const starts: string[] = []; + const calls: string[] = []; + const profile = createAnalysisProfile(IDENTITY, { fixture: true }); + const provider = makeProvider(starts, calls, profile); + const initialDependencies: DirectAnalysisDependencies = { + createBinarySession: () => createTestBinarySession(provider), + createManagedBinarySession: () => createTestBinarySession(provider), + }; + await runDirectAnalysis( + initialDependencies, + path, + "binary_overview", + {}, + { snapshotPath }, + ); + + const controller = new AbortController(); + const router = SessionProviderRouter.single(provider); + const resolve = router.resolve.bind(router); + router.resolve = async (...arguments_) => { + const resolved = await resolve(...arguments_); + controller.abort(); + return resolved; + }; + const session = new BinarySession(router); + const dependencies: DirectAnalysisDependencies = { + createBinarySession: () => session, + createManagedBinarySession: () => session, + }; + const result = await runDirectAnalysis( + dependencies, + path, + "binary_overview", + {}, + { snapshotPath, signal: controller.signal }, + ); + + expect(result).toMatchObject({ + error: "Analysis failed", + code: "cancelled", + }); + expect(calls).toEqual(["health", ...operations]); + expect(starts).toHaveLength(1); + }); +}); diff --git a/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts b/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts index d4d60e83b3..3f14ac5b40 100644 --- a/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts +++ b/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts @@ -7,13 +7,102 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; -import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js"; +import { + analyzeJavaScriptArtifactFiles, + analyzeAndProjectJavaScriptArtifactFiles, +} from "../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { buildJavaScriptArtifactGraph } from "../../../src/application/javascript/JavaScriptArtifactGraphBuilder.js"; +import { + buildJavaScriptSemanticGraph, + createJavaScriptSemanticGraphProjection, +} from "../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; +import { createJavaScriptArtifactReader } from "../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; +import { readJavaScriptArtifactFiles } from "../../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; +import { scanCanonicalArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js"; +import { scanArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { parseJavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createJavaScriptSemanticGraph } from "../../../src/domain/javascript/javascriptSemanticGraph.js"; import { parseJavaScriptSemanticGraph } from "../../../src/domain/javascript/javascriptSemanticGraphSerialization.js"; import { writeJavaScriptArtifactFixture } from "../../fixtures/javascriptArtifactApplication.js"; +it("reports file progress and honors cancellation before the next source", async () => { + const root = await fixtureDirectory(); + const controller = new AbortController(); + const progress: string[] = []; + await expect( + reconstructJavaScriptArtifact({ input_path: root }, controller.signal, { + report: async (event) => { + if (event.phase === "parse_javascript_source") { + progress.push(event.message); + if (progress.length === 2) controller.abort(); + } + }, + }), + ).rejects.toMatchObject({ reason: "cancelled" }); + expect(progress).toHaveLength(2); + expect(progress[0]).not.toBe(progress[1]); +}); + +it("preserves graph commitments and export shapes when consuming file-local IR", async () => { + const root = await fixtureDirectory(); + await writeFile( + join(root, "exports.mjs"), + ` + import { readFile } from "node:fs"; + export { readFile as read }; + export function create(value) { return { value, nested: { enabled: true } }; } + function unexported() { return "private"; } + `, + ); + const snapshot = await scanCanonicalArtifactInventory(root, {}); + const reader = createJavaScriptArtifactReader(root, "directory"); + try { + const files = await readJavaScriptArtifactFiles(reader, snapshot); + const full = analyzeJavaScriptArtifactFiles(files); + const projection = createJavaScriptSemanticGraphProjection(); + const compact = await analyzeAndProjectJavaScriptArtifactFiles( + files, + projection.projectFile, + ); + const graph = buildJavaScriptArtifactGraph(snapshot, files, full); + expect(buildJavaScriptArtifactGraph(snapshot, files, compact)).toEqual( + graph, + ); + const semantic = projection.finish( + snapshot.manifest.root_sha256, + graph, + compact, + ); + expect(semantic).toEqual( + buildJavaScriptSemanticGraph({ + rootArtifactSha256: snapshot.manifest.root_sha256, + applicationGraph: graph, + analysis: full, + }), + ); + expect( + semantic.nodes.some( + ({ application_node_ids }) => application_node_ids.length > 0, + ), + ).toBe(true); + const exported = compact.files.find( + ({ file }) => file.path === "exports.mjs", + ); + expect(exported?.semantic?.ir.callables.map(({ name }) => name)).toEqual([ + "create", + ]); + expect( + full.files + .find(({ file }) => file.path === "exports.mjs") + ?.semantic?.ir.callables.map(({ name }) => name), + ).toContain("unexported"); + expect(parseJavaScriptSemanticGraph(semantic)).toEqual(semantic); + } finally { + await reader.close(); + } +}); + it("reconstructs package, Electron roles, Webpack/Rspack modules, and cross-layer facts without execution", async () => { const root = await fixtureDirectory(); Reflect.deleteProperty(globalThis, "__rea_bundle_executed"); diff --git a/tests/conformance/contracts/analysisErrorProjection.test.ts b/tests/conformance/contracts/analysisErrorProjection.test.ts index 228cd7ca8f..8eb58e5a31 100644 --- a/tests/conformance/contracts/analysisErrorProjection.test.ts +++ b/tests/conformance/contracts/analysisErrorProjection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { ProcessCaptureError } from "../../../src/application/ProcessCaptureError.js"; +import { ProcessCaptureError } from "../../../src/process/capture/ProcessCaptureError.js"; import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js"; import { ArtifactOperationError } from "../../../src/domain/artifactOperationError.js"; import { EvidenceFileError } from "../../../src/domain/evidenceErrors.js"; @@ -25,7 +25,7 @@ describe("analysis error projection contract", () => { ).map( (reason) => new ArtifactOperationError("inventory_artifact", reason), ), - ...(["not-file", "exists", "invalid-json", "io"] as const).map( + ...(["not-file", "exists", "invalid-json", "missing", "io"] as const).map( (reason) => new EvidenceFileError("read", reason), ), ...( diff --git a/tests/conformance/native/calls.m b/tests/conformance/native/calls.m new file mode 100644 index 0000000000..5f69abeca2 --- /dev/null +++ b/tests/conformance/native/calls.m @@ -0,0 +1,33 @@ +#import +#include +#include +#include + +// Observed by verify:native-calls; `--wait` keeps the process running. +@interface ReaCallGreeter : NSObject +- (NSString *)greet:(NSString *)name times:(long)count; +@end +@implementation ReaCallGreeter +- (NSString *)greet:(NSString *)name times:(long)count { + return [NSString stringWithFormat:@"%@ x%ld", name, count]; +} +@end + +__attribute__((noinline)) int rea_call_add(int left, int right) { + return left + right; +} + +int main(int argc, char **argv) { + @autoreleasepool { + if (argc > 1 && strcmp(argv[1], "--wait") == 0) + for (;;) sleep(1); + ReaCallGreeter *greeter = [ReaCallGreeter new]; + for (long index = 0; index < 3; index++) + printf("%s %d\n", [[greeter greet:@"world" times:index] UTF8String], + rea_call_add((int)index, 40)); + const char *marker = getenv("REA_CALLS_MARKER"); + if (marker != NULL) + printf("marker %s\n", marker); + } + return 0; +} diff --git a/tests/conformance/native/keyed-archive-hierarchy.swift b/tests/conformance/native/keyed-archive-hierarchy.swift new file mode 100644 index 0000000000..a562257de2 --- /dev/null +++ b/tests/conformance/native/keyed-archive-hierarchy.swift @@ -0,0 +1,17 @@ +import AppKit + +let count = Int(CommandLine.arguments[2]) ?? 40 +let root = NSView(frame: .zero) +root.identifier = NSUserInterfaceItemIdentifier("root") +var parent = root +for index in 0.. = captureChildValues(requestedCount: 2) { nil } precondition(failed.values.isEmpty, "AX retrieval failure must return no values") precondition(!failed.complete, "AX retrieval failure must mark traversal incomplete") @@ -15,4 +40,4 @@ let complete = captureChildValues(requestedCount: 2) { [10, 20, 30] } precondition(complete.values == [10, 20], "Retrieval must respect the requested node budget") precondition(complete.complete, "A full bounded result must remain complete") -print("Native UI child retrieval seam passed") +print("Native UI child retrieval and serialization seams passed") diff --git a/tests/conformance/providers/conformanceFixtures.test.ts b/tests/conformance/providers/conformanceFixtures.test.ts deleted file mode 100644 index 4bb5ca6ee7..0000000000 --- a/tests/conformance/providers/conformanceFixtures.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { sourceDigest } from "../../../scripts/lib/conformance-fixtures.mjs"; - -describe("source-built conformance fixtures", () => { - it("hashes source manifests independently of input ordering", () => { - const left = [ - { path: "b.c", content: "b" }, - { path: "a.c", content: "a" }, - ]; - const right = [...left].reverse(); - - expect(sourceDigest(left)).toBe(sourceDigest(right)); - expect(sourceDigest(left)).toMatch(/^[0-9a-f]{64}$/u); - }); -}); diff --git a/tests/conformance/providers/conformanceTrustGate.test.ts b/tests/conformance/providers/conformanceTrustGate.test.ts index 4de72fb966..bb581b8d3c 100644 --- a/tests/conformance/providers/conformanceTrustGate.test.ts +++ b/tests/conformance/providers/conformanceTrustGate.test.ts @@ -4,8 +4,6 @@ import type { VerifierContract } from "../../../src/domain/conformancePackage.js import { compareDimension, evaluateTrustGate, - isSemanticDimension, - isVolatileDimension, trustGateResultSchema, } from "../../../src/domain/conformanceTrustGate.js"; @@ -35,52 +33,6 @@ const expectDivergenceRejected = ( }; describe("conformance trust gates", () => { - it("classifies volatile dimensions correctly", () => { - expect(isVolatileDimension("timing")).toBe(true); - expect(isVolatileDimension("timestamp")).toBe(true); - expect(isVolatileDimension("pid")).toBe(true); - expect(isVolatileDimension("exit_code")).toBe(false); - }); - - it("classifies semantic dimensions correctly", () => { - expect(isSemanticDimension("exit_code")).toBe(true); - expect(isSemanticDimension("stdout")).toBe(true); - expect(isSemanticDimension("timing")).toBe(false); - }); - - it("matches identical values", () => { - const result = compareDimension("exit_code", 0, 0); - expect(result.status).toBe("match"); - }); - - it("detects mismatches", () => { - const result = compareDimension("exit_code", 0, 1); - expect(result.status).toBe("mismatch"); - }); - - it("ignores volatile dimensions", () => { - const result = compareDimension("timing", 100, 200); - expect(result.status).toBe("match"); - expect(result.message).toContain("volatile"); - }); - - it("handles truncated evidence", () => { - const result = compareDimension("exit_code", 0, null, { - truncated: true, - }); - expect(result.status).toBe("truncated"); - }); - - it("handles undefined values", () => { - const result = compareDimension("exit_code", undefined, undefined); - expect(result.status).toBe("unknown"); - }); - - it("handles missing actual", () => { - const result = compareDimension("exit_code", 0, undefined); - expect(result.status).toBe("unknown"); - }); - it("keeps semantic unknown and truncated values out of equivalence", () => { expect( compareDimension( diff --git a/tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts b/tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts deleted file mode 100644 index 8d8573025d..0000000000 --- a/tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { execFile } from "node:child_process"; -import { createHash } from "node:crypto"; -import { readFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { promisify } from "node:util"; -import { fileURLToPath } from "node:url"; - -import { describe, expect, it } from "vitest"; - -import { parseExecutableHeader } from "../../../../src/domain/binaryTarget.js"; - -const exec = promisify(execFile); -const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../../.."); -const generator = resolve(root, "scripts", "create-ghidra-windows-fixture.mjs"); -const fixture = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe"); -const expectedSha256 = - "732ec6083ae3b63bcbda33f2d0fd7e1a6539babade07cbdae4fc20d3394faf49"; - -describe("controlled Windows Ghidra fixture", () => { - it("generates one deterministic native x86-64 PE application", async () => { - await exec(process.execPath, [generator]); - const bytes = await readFile(fixture); - - expect(createHash("sha256").update(bytes).digest("hex")).toBe( - expectedSha256, - ); - expect(parseExecutableHeader(bytes, "x64")).toMatchObject({ - ok: true, - value: { - format: "pe", - architecture: "x86_64", - executableRole: "application", - managed: false, - }, - }); - }); -}); diff --git a/tests/conformance/providers/managed/managedConformanceFixture.test.ts b/tests/conformance/providers/managed/managedConformanceFixture.test.ts deleted file mode 100644 index 4899a3decf..0000000000 --- a/tests/conformance/providers/managed/managedConformanceFixture.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { rm, writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; - -import { buildManagedPeFixture } from "../../../../scripts/fixtures/managed/pe.mjs"; -import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; - -const workspaces: string[] = []; - -afterEach(async () => { - await Promise.all( - workspaces.splice(0).map((path) => rm(path, { recursive: true })), - ); -}); - -describe("managed conformance fixture", () => { - it("emits PE32+ headers when the COFF machine is x86-64", async () => { - const workspace = await createTestTempDirectory("rea-managed-fixture-"); - workspaces.push(workspace); - const path = join(workspace, "fixture.exe"); - await writeFile(path, buildManagedPeFixture({ machine: 0x8664 })); - - const parsed = await parseBinaryTarget(path); - - expect(parsed).toMatchObject({ - ok: true, - value: { - format: "pe", - architecture: "x86_64", - managed: true, - }, - }); - }); -}); diff --git a/tests/conformance/readmeConsistency.test.ts b/tests/conformance/readmeConsistency.test.ts index 7ff09e4097..42dfe1db8f 100644 --- a/tests/conformance/readmeConsistency.test.ts +++ b/tests/conformance/readmeConsistency.test.ts @@ -5,8 +5,8 @@ import { describe, expect, it } from "vitest"; import { SUPPORTED_CLIENT_DEFINITIONS } from "../../src/application/SupportedClients.js"; import { PRODUCT_IDENTITY } from "../../src/identity.js"; -const readmes = [ - "README.md", +const setupDocumentation = [ + "docs/installation.md", "README_zh.md", "README_ja.md", "README_ko.md", @@ -21,8 +21,8 @@ const jsonExamples = (content: string): unknown[] => JSON.parse(match[1] ?? ""), ); -describe("localized README product facts", () => { - it.each(readmes)( +describe("onboarding documentation product facts", () => { + it.each(setupDocumentation)( "keeps requirements and versioned MCP configuration aligned in %s", async (path) => { const content = await readFile(resolve(path), "utf8"); @@ -45,9 +45,11 @@ describe("localized README product facts", () => { expect(content).toContain("Ubuntu 24.04"); expect(content).toContain("Fedora 41"); expect(content).toContain("Arch Linux"); + expect(content).toContain("CachyOS"); for (const client of SUPPORTED_CLIENT_DEFINITIONS) expect(content).toContain(client.displayName); - expect(content).toContain("MCP-tool_catalog"); + if (path.startsWith("README_")) + expect(content).toContain("MCP-tool_catalog"); }, ); diff --git a/tests/conformance/releaseConfiguration.test.ts b/tests/conformance/releaseConfiguration.test.ts index 6988de6f94..ddcadac614 100644 --- a/tests/conformance/releaseConfiguration.test.ts +++ b/tests/conformance/releaseConfiguration.test.ts @@ -8,7 +8,6 @@ const readJson = async (path: string): Promise => JSON.parse(await readFile(path, "utf8")) as unknown; const versionedDocumentation = [ - "README.md", "README_zh.md", "README_ja.md", "README_ko.md", @@ -48,11 +47,6 @@ describe("release configuration", () => { path: "server.json", jsonpath: "$.packages[0].version", }, - { - type: "json", - path: "docs/product-catalog.json", - jsonpath: "$.package.version", - }, ], }, }, @@ -63,15 +57,33 @@ describe("release configuration", () => { "marks the versioned package example in %s", async (path) => { const content = await readFile(path, "utf8"); + const packageJson = (await readJson("package.json")) as { + version: string; + }; const versionBlock = /[\s\S]*?/u.exec( content, )?.[0]; - expect(versionBlock).toMatch(/rea-agents@\d+\.\d+\.\d+/u); + expect(versionBlock).toContain(`rea-agents@${packageJson.version}`); }, ); + it("keeps the release baseline, lockfile, and changelog aligned with the package version", async () => { + const packageJson = (await readJson("package.json")) as { + version: string; + }; + expect(await readJson(".release-please-manifest.json")).toEqual({ + ".": packageJson.version, + }); + expect(await readJson("package-lock.json")).toMatchObject({ + version: packageJson.version, + packages: { "": { version: packageJson.version } }, + }); + const changelog = await readFile("CHANGELOG.md", "utf8"); + expect(/^## \[([^\]]+)\]/mu.exec(changelog)?.[1]).toBe(packageJson.version); + }); + it("keeps the npm package and MCP Registry metadata aligned", async () => { const packageJson = (await readJson("package.json")) as { name: string; diff --git a/tests/evaluation/model-evals/codexAgentEval.test.ts b/tests/evaluation/model-evals/codexAgentEval.test.ts index c068878f60..82100cea6b 100644 --- a/tests/evaluation/model-evals/codexAgentEval.test.ts +++ b/tests/evaluation/model-evals/codexAgentEval.test.ts @@ -100,6 +100,29 @@ describe("Codex agent release evaluation", () => { .repeatedCallCount, ).toBe(1); }); + + it("counts identical arguments with reversed Unicode key insertion order", () => { + const call = (id: string, arguments_: Record) => ({ + type: "item.completed", + item: { + id, + type: "mcp_tool_call", + server: "rea", + tool: "inspect_artifact", + arguments: arguments_, + }, + }); + + expect( + evaluateCodexEvents( + [ + call("item-1", { "e\u0301": 1, "\u00e9": 2 }), + call("item-2", { "\u00e9": 2, "e\u0301": 1 }), + ], + "inspect_artifact", + ).repeatedCallCount, + ).toBe(1); + }); }); describe("Codex agent completion evaluation", () => { diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index 8dca270636..c9b1a25c82 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -8,6 +8,28 @@ import { describe, expect, it } from "vitest"; import { inspectModuleBoundaries } from "../../scripts/lib/module-boundaries.mjs"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +describe("process capture implementation ownership", () => { + it.each([ + ["../../application/process/ProcessEvidence.js", false], + ["../../composition/binary.js", false], + ["../../server/createServer.js", false], + ["../../cli.ts", false], + ["../../main.js", false], + ["../../domain/process/processEvidenceProvider.js", true], + ["../ProcessOwnership.js", true], + ["../../windows/WindowsOwnedProcess.js", true], + ["./TerminalRenderer.js", true], + ])("checks capture importing %s", (dependency, allowed) => { + const violations = inspectModuleBoundaries( + "src/process/capture/ProcessHarness.ts", + `import { value } from "${dependency}";`, + process.cwd(), + ); + if (allowed) expect(violations).toEqual([]); + else expect(violations).toMatchObject([{ boundary: "process-capture" }]); + }); +}); + describe("binary production construction ownership", () => { it.each([ "hopper/HopperProvider.js", @@ -54,7 +76,7 @@ describe("incremental module import boundaries", () => { ["src/contracts/probe.ts", "../composition/android.js", false], [ "src/artifacts/javascript/probe.ts", - "../../application/ArtifactInventory.js", + "../../application/javascript/JavaScriptArtifactReconstruction.js", false, ], [ @@ -65,7 +87,7 @@ describe("incremental module import boundaries", () => { ["src/artifacts/javascript/probe.ts", "../../cli.js", false], [ "src/artifacts/ArtifactHash.ts", - "../application/ArtifactInventory/hash.js", + "../application/artifacts/ArtifactExtractionDestination.js", false, ], [ @@ -89,11 +111,7 @@ describe("incremental module import boundaries", () => { "../../artifacts/javascript/JavaScriptArtifactReader.js", true, ], - [ - "src/application/ArtifactInventory/probe.ts", - "../../artifacts/AsarArtifactReader.js", - true, - ], + ["src/artifacts/inventory/probe.ts", "../AsarArtifactReader.js", true], ["src/domain/probe.test.ts", "../composition/firmware.js", false], ["src/application/probe.ts", "../android/JadxProvider.js", false], ["src/application/probe.ts", "../composition/android.js", false], @@ -307,21 +325,30 @@ describe("test lane import boundaries", () => { }); }); -describe("Apple artifact producer ownership", () => { - it.each([ - ["../../application/Workflow.js", false], - ["../../composition/android.js", false], - ["../../cli.js", false], - ["../../domain/apple/plistValue.js", true], - ["../DirectoryArtifactReader.js", true], - ])("checks the producer dependency %s", (dependency, allowed) => { - const violations = inspectModuleBoundaries( - "src/artifacts/apple/probe.ts", - `import type { Value } from ${JSON.stringify(dependency)};`, - process.cwd(), - ); +describe.each(["apple", "inventory", "extraction"])( + "%s artifact producer ownership", + (owner) => { + it.each([ + ["../../application/Workflow.js", false], + ["../../composition/android.js", false], + ["../../cli.js", false], + ["../../server/registerEvidenceTools.js", false], + ["../../main.ts", false], + ["../../domain/apple/plistValue.js", true], + ["../../domain/artifactInventorySnapshot.js", true], + ["../../contracts/artifactGraph.js", true], + ["../DirectoryArtifactReader.js", true], + ["../ArtifactHash.js", true], + ])("checks the producer dependency %s", (dependency, allowed) => { + const violations = inspectModuleBoundaries( + `src/artifacts/${owner}/probe.ts`, + `import type { Value } from ${JSON.stringify(dependency)};`, + process.cwd(), + ); - expect(violations.length === 0).toBe(allowed); - if (!allowed) expect(violations[0]?.boundary).toBe("artifact-acquisition"); - }); -}); + expect(violations.length === 0).toBe(allowed); + if (!allowed) + expect(violations[0]?.boundary).toBe("artifact-acquisition"); + }); + }, +); diff --git a/tests/evaluation/package/packageSetupSupport.test.ts b/tests/evaluation/package/packageSetupSupport.test.ts deleted file mode 100644 index 2d803ef589..0000000000 --- a/tests/evaluation/package/packageSetupSupport.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { packageSetupSupport } from "../../../scripts/verify-package-discovery.mjs"; - -describe("packaged setup support classification", () => { - it.each([ - ["linux", true, true, true, true], - ["linux", true, false, true, false], - ["linux", false, true, false, false], - ["linux", false, false, false, false], - ["darwin", true, true, true, true], - ["darwin", true, false, true, false], - ["darwin", false, true, false, false], - ["darwin", false, false, false, false], - ["win32", true, true, true, false], - ["win32", true, false, true, false], - ["win32", false, true, false, false], - ["win32", false, false, false, false], - ] as const)( - "%s with node=%s and host=%s: agent setup=%s, Hopper setup=%s", - (platform, node, host, supportedSetupHost, hopperSetupSupported) => { - expect( - packageSetupSupport( - [ - { name: "host", ok: host }, - { name: "node", ok: node }, - ], - platform, - ), - ).toEqual({ supportedSetupHost, hopperSetupSupported }); - }, - ); - - it.each([ - { checks: undefined }, - { checks: [] }, - { checks: [{ name: "host", ok: true }] }, - ])("requires an affirmative Node check: $checks", ({ checks }) => { - expect(packageSetupSupport(checks, "linux")).toEqual({ - supportedSetupHost: false, - hopperSetupSupported: false, - }); - }); - - it("requires an affirmative host check only for Hopper setup", () => { - expect( - packageSetupSupport( - [ - { name: "node", ok: true }, - { name: "hopper", ok: true }, - ], - "linux", - ), - ).toEqual({ supportedSetupHost: true, hopperSetupSupported: false }); - }); -}); diff --git a/tests/fixtures/abandonStoreFileLock.mjs b/tests/fixtures/abandonStoreFileLock.mjs deleted file mode 100644 index 1022261f13..0000000000 --- a/tests/fixtures/abandonStoreFileLock.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { createStoreFileLock } from "../../dist/application/StoreFileLock.js"; - -const path = process.argv[2]; -if (path === undefined) throw new Error("A lock path is required"); -await createStoreFileLock(path); -// Exit without invoking release so the parent can exercise dead-owner recovery. diff --git a/tests/fixtures/binaryDiagnostics/layout.ts b/tests/fixtures/binaryDiagnostics/layout.ts new file mode 100644 index 0000000000..28cbf03c12 --- /dev/null +++ b/tests/fixtures/binaryDiagnostics/layout.ts @@ -0,0 +1,48 @@ +import type { BinaryLayout } from "../../../src/domain/native/binaryLayout.js"; + +/** Small source-owned provider observation for validation and transport tests. */ +export const binaryLayoutFixture = ( + path = "/artifacts/source-owned.elf", +): BinaryLayout => ({ + artifact: { path, sha256: "a".repeat(64), bytes: 128 }, + format: "elf", + architecture: { machine: "EM_X86_64", bits: 64, byte_order: "little" }, + image_type: "ET_REL", + entry_point: { + reported_value: "0x0", + meaning: "not-applicable", + execution_status: "unknown", + }, + runtime_load_base: null, + sections: [], + segments: [], + symbols: [], + relocations: [], + packed_relative_relocations: [], + relocation_inventory_completeness: "unknown", + linkage: { + needed_libraries: [], + interpreters: [], + got: [], + plt: [], + convenience_maps_completeness: "unknown", + runtime_library_paths: null, + }, + mitigations: { + evidence_kind: "inferred", + position_independent: false, + nx_indicator: null, + executable_stack_indicator: false, + stack_canary_indicator: false, + relro: null, + }, + diagnostics: { stdout: "producer warning", stderr: "", truncated: false }, + limitations: ["Runtime addresses remain unknown."], +}); + +/** Producer identity belongs to the test seam, not a simulated installed engine claim. */ +export const BINARY_LAYOUT_TEST_PROVIDER = { + id: "fixture-layout", + name: "Source-owned layout seam", + version: "1", +} as const; diff --git a/tests/fixtures/binaryDiagnostics/recordedCrash.ts b/tests/fixtures/binaryDiagnostics/recordedCrash.ts new file mode 100644 index 0000000000..636a20c304 --- /dev/null +++ b/tests/fixtures/binaryDiagnostics/recordedCrash.ts @@ -0,0 +1,220 @@ +import type { RecordedCrash } from "../../../src/domain/native/recordedCrash.js"; + +/** Source-owned observation seam; these bytes do not simulate a verified ELF engine. */ +export const recordedCrashFixture = ( + path = "/artifacts/source-owned.core", +): RecordedCrash => { + const descriptor = Buffer.alloc(336); + descriptor.writeInt32LE(123, 32); + descriptor.writeInt16LE(11, 12); + descriptor.writeBigUInt64LE(0x1122334455667788n, 224); + return { + artifact: { path, sha256: "a".repeat(64), bytes: 512 }, + format: "elf-core", + architecture: "x86_64-little-endian", + target_execution: "not-performed", + live_process_identity: "unknown", + segments: [ + { + index: 0, + type: "PT_NOTE", + header_location: { offset: "0x40", bytes: "0x38" }, + offset: "0x80", + file_size: "0x164", + memory_size: "0x0", + virtual_address: "0x0", + physical_address: "0x0", + alignment: "0x4", + flags: "0x0", + file_backing: "file", + }, + ], + notes: [ + { + index: 0, + segment_index: 0, + type: "NT_PRSTATUS", + location: { offset: "0x80", bytes: "0x164" }, + owner_location: { offset: "0x8c", bytes: "0x5" }, + owner_bytes_base64: Buffer.from("CORE\0").toString("base64"), + owner_display: "CORE", + descriptor_location: { offset: "0x94", bytes: "0x150" }, + descriptor_bytes_base64: descriptor.toString("base64"), + }, + ], + note_padding: [], + threads: [ + { + note_index: 0, + historical_pid: 123, + recorded_current_signal: 11, + registers: [ + "r15", + "r14", + "r13", + "r12", + "rbp", + "rbx", + "r11", + "r10", + "r9", + "r8", + "rax", + "rcx", + "rdx", + "rsi", + "rdi", + "orig_rax", + "rip", + "cs", + "eflags", + "rsp", + "ss", + "fs_base", + "gs_base", + "ds", + "es", + "fs", + "gs", + ].map((name, index) => ({ + name, + value: name === "rdi" ? "0x1122334455667788" : "0x0", + location: { + offset: `0x${(148 + 112 + index * 8).toString(16)}`, + bytes: "0x8", + }, + })), + }, + ], + signals: [], + debugger: { status: "not-requested" }, + note_interpretation_completeness: "unknown", + decoder_diagnostics: { + stdout: "decoder diagnostic", + stderr: "", + truncated: false, + }, + diagnostics: { stdout: "decoder diagnostic", stderr: "", truncated: false }, + limitations: ["Historical PID is recorded metadata."], + }; +}; + +/** Source-owned SIGSEGV descriptor for scalar/source binding regressions. */ +export const recordedCrashSignalFixture = (path?: string): RecordedCrash => { + const value = recordedCrashFixture(path); + const descriptor = Buffer.alloc(32); + descriptor.writeInt32LE(11, 0); + descriptor.writeInt32LE(-2, 4); + descriptor.writeInt32LE(1, 8); + descriptor.writeBigUInt64LE(0x1122334455667788n, 16); + value.artifact.bytes = 640; + value.segments = value.segments.map((segment) => ({ + ...segment, + file_size: "0x198", + })); + value.notes.push({ + index: 1, + segment_index: 0, + type: "NT_SIGINFO", + location: { offset: "0x1e4", bytes: "0x34" }, + owner_location: { offset: "0x1f0", bytes: "0x5" }, + owner_bytes_base64: Buffer.from("CORE\0").toString("base64"), + owner_display: "CORE", + descriptor_location: { offset: "0x1f8", bytes: "0x20" }, + descriptor_bytes_base64: descriptor.toString("base64"), + }); + value.signals.push({ + note_index: 1, + number: 11, + code: 1, + errno: -2, + fault_address: "0x1122334455667788", + fault_address_meaning: "recorded-sigsegv-address", + thread_association: "unknown", + }); + return value; +}; + +/** Construct only the original byte ranges consumed by the provider protocol tests. */ +export const recordedCrashFixtureBytes = (value: RecordedCrash): Buffer => { + const bytes = Buffer.alloc(value.artifact.bytes); + bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]); + bytes.writeUInt16LE(4, 16); + bytes.writeUInt16LE(62, 18); + bytes.writeBigUInt64LE(64n, 32); + bytes.writeUInt16LE(56, 54); + bytes.writeUInt16LE(value.segments.length, 56); + for (const segment of value.segments) { + const start = Number(BigInt(segment.header_location.offset)); + bytes.writeUInt32LE(segment.type === "PT_NOTE" ? 4 : 0, start); + bytes.writeUInt32LE(Number(BigInt(segment.flags)), start + 4); + for (const [field, offset] of [ + [segment.offset, 8], + [segment.virtual_address, 16], + [segment.physical_address, 24], + [segment.file_size, 32], + [segment.memory_size, 40], + [segment.alignment, 48], + ] as const) + bytes.writeBigUInt64LE(BigInt(field), start + offset); + } + for (const note of value.notes) { + const start = Number(BigInt(note.location.offset)); + bytes.writeUInt32LE(Number(BigInt(note.owner_location.bytes)), start); + bytes.writeUInt32LE( + Number(BigInt(note.descriptor_location.bytes)), + start + 4, + ); + bytes.writeUInt32LE( + note.type === "NT_PRSTATUS" ? 1 : 0x53494749, + start + 8, + ); + Buffer.from(note.owner_bytes_base64, "base64").copy( + bytes, + Number(BigInt(note.owner_location.offset)), + ); + Buffer.from(note.descriptor_bytes_base64, "base64").copy( + bytes, + Number(BigInt(note.descriptor_location.offset)), + ); + } + return bytes; +}; + +export const RECORDED_CRASH_TEST_PROVIDER = { + id: "fixture-recorded-core", + name: "Source-owned recorded-core seam", + version: "1", +} as const; + +/** Source-owned debugger representation with deliberately unreported permission flags. */ +export const recordedCrashDebuggerFixture = (): Extract< + RecordedCrash["debugger"], + { status: "available" } +> => ({ + status: "available", + gdb_version: "source seam", + pwndbg_version: "source seam", + connection: "core", + executable: null, + historical_pid: 123, + reported_limits: { + address_space_bytes: 3221225472, + cpu_seconds: 30, + file_size_bytes: 67108864, + }, + maps: [ + { + start: "0x1000", + end: "0x2000", + offset: "0x0", + reported_flags: 0, + permissions: null, + pathname_display: "recorded/path", + current_file_identity: "unknown", + }, + ], + confidence: "derived", + completeness: "unknown", + diagnostics: { stdout: "", stderr: "", truncated: false }, +}); diff --git a/tests/fixtures/evm/interface.ts b/tests/fixtures/evm/interface.ts new file mode 100644 index 0000000000..23bfcfa026 --- /dev/null +++ b/tests/fixtures/evm/interface.ts @@ -0,0 +1,44 @@ +import { createHash } from "node:crypto"; +import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; +import type { EvmInterface } from "../../../src/domain/evm/evmInterface.js"; + +/** Source-owned transport seam, independent of real engine verification. */ +export const evmInterfaceFixture = ( + path = "/artifacts/source-owned.hex", +): EvmInterface => ({ + artifact: { path, sha256: "a".repeat(64), bytes: 10, encoding: "hex" }, + bytecode: { + sha256: createHash("sha256") + .update(Buffer.from("60006000f3", "hex")) + .digest("hex"), + bytes: 5, + hex: "60006000f3", + digest_algorithm: "sha256", + kind: "unknown", + deployment_authenticity: "unknown", + hardfork: "unknown", + }, + evidence_kind: "inferred", + functions: [], + discovery_completeness: "unknown", + runtime_execution: "not-performed", + diagnostics: { stdout: "", stderr: "", truncated: false }, + limitations: ["Interface completeness remains unknown."], +}); + +/** Bind a fixture to its explicit producing seam and carrier identity. */ +export const evmInterfaceExecution = (value = evmInterfaceFixture()) => + createAnalysisExecution( + value, + { id: "fixture-evm", name: "Source-owned EVM seam", version: "1" }, + { + rawResult: { functions: [] }, + subject: { + path: value.artifact.path, + format: "file", + sha256: value.artifact.sha256, + }, + limitations: value.limitations, + locations: [{ kind: "artifact-path", path: value.artifact.path }], + }, + ); diff --git a/tests/fixtures/fakeCdpBrowserEvents.ts b/tests/fixtures/fakeCdpBrowserEvents.ts index 3a2c5bde89..7d54e66bf2 100644 --- a/tests/fixtures/fakeCdpBrowserEvents.ts +++ b/tests/fixtures/fakeCdpBrowserEvents.ts @@ -282,6 +282,133 @@ const emitSessionTimeline = ( } }; +const emitSameOriginRedirectEvents = ( + socket: WebSocket, + command: FakeCdpCommand, + port: number, + url: string, +): string => { + const intermediate = `http://127.0.0.1:${String(port)}/intermediate`; + const finalUrl = `http://127.0.0.1:${String(port)}/redirected?token=final-secret`; + event(socket, "Network.requestWillBeSent", command.sessionId, { + requestId: "request-1", + type: "Fetch", + request: { url: intermediate, method: "GET" }, + redirectResponse: { + url, + status: 301, + mimeType: "text/plain", + encodedDataLength: 23, + headers: { "X-Private": "redirect-header-secret" }, + }, + timestamp: 8, + }); + event(socket, "Network.requestWillBeSent", command.sessionId, { + requestId: "request-1", + type: "Fetch", + request: { url: finalUrl, method: "GET" }, + redirectResponse: { + url: intermediate, + status: 302, + mimeType: "text/plain", + encodedDataLength: 27, + }, + timestamp: 9, + }); + return finalUrl; +}; + +const emitReturnFromDisallowedRedirect = ( + socket: WebSocket, + command: FakeCdpCommand, + port: number, +): string => { + const finalUrl = `http://127.0.0.1:${String(port)}/returned`; + event(socket, "Network.requestWillBeSent", command.sessionId, { + requestId: "request-1", + type: "Fetch", + request: { url: finalUrl, method: "GET" }, + redirectResponse: { + url: "https://private.example.test/outside?token=redirect-url-secret", + status: 302, + mimeType: "text/plain", + encodedDataLength: 31, + headers: { "X-Private": "redirect-header-secret" }, + postData: "redirect-body-secret", + }, + timestamp: 8, + }); + return finalUrl; +}; + +const emitMalformedRedirectPriorResponse = ( + socket: WebSocket, + command: FakeCdpCommand, + port: number, + initialUrl: string, +): void => { + const priorUrl = `http://127.0.0.1:${String(port)}/malformed-redirect-prior`; + event(socket, "Network.requestWillBeSent", command.sessionId, { + requestId: "request-1", + type: "Fetch", + request: { + url: priorUrl, + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + postData: JSON.stringify({ + operation: "preserve-prior", + token: "prior-body", + }), + }, + redirectResponse: { + url: initialUrl, + status: 307, + mimeType: "text/plain", + encodedDataLength: 23, + }, + timestamp: 8, + }); + event(socket, "Network.responseReceived", command.sessionId, { + requestId: "request-1", + response: { + url: priorUrl, + status: 201, + mimeType: "application/json", + headers: { + "Content-Length": "123", + "Content-Encoding": "gzip", + "Content-Security-Policy": "default-src 'self'", + "X-Model-Context": "prior-context-hint", + }, + }, + }); +}; + +const emitMalformedRedirect = ( + socket: WebSocket, + command: FakeCdpCommand, + port: number, + options: FakeOptions, +): string => { + const finalUrl = `http://127.0.0.1:${String(port)}/malformed-redirect-final`; + const redirectResponse = Object.hasOwn(options, "redirectResponseEnvelope") + ? options.redirectResponseEnvelope + : { + ...(options.redirectResponseUrl === undefined + ? {} + : { url: options.redirectResponseUrl }), + status: 302, + }; + event(socket, "Network.requestWillBeSent", command.sessionId, { + requestId: "request-1", + type: "Fetch", + request: { url: finalUrl, method: "GET" }, + redirectResponse, + timestamp: 9, + }); + return finalUrl; +}; + const emitNetworkEvents = ( socket: WebSocket, command: FakeCdpCommand, @@ -297,11 +424,15 @@ const emitNetworkEvents = ( }, }); const url = `http://127.0.0.1:${String(port)}/api?token=network-secret`; + const requestUrl = + options.redirectFromDisallowedOrigin === true + ? "https://private.example.test/start?token=initial-url-secret" + : url; event(socket, "Network.requestWillBeSent", command.sessionId, { requestId: "request-1", type: "Fetch", request: { - url, + url: requestUrl, method: "POST", headers: { Authorization: "Bearer request-secret", @@ -313,6 +444,7 @@ const emitNetworkEvents = ( filters: { active: true }, }), }, + timestamp: 7, initiator: { type: "script", stack: { @@ -326,6 +458,16 @@ const emitNetworkEvents = ( }, }, }); + if (options.malformedRedirectResponse === true) + emitMalformedRedirectPriorResponse(socket, command, port, url); + const responseUrl = + options.redirectFromDisallowedOrigin === true + ? emitReturnFromDisallowedRedirect(socket, command, port) + : options.malformedRedirectResponse === true + ? emitMalformedRedirect(socket, command, port, options) + : options.redirectWithinOrigin === true + ? emitSameOriginRedirectEvents(socket, command, port, url) + : url; if (options.redirectToDisallowedOrigin === true) event(socket, "Network.requestWillBeSent", command.sessionId, { requestId: "request-1", @@ -334,14 +476,22 @@ const emitNetworkEvents = ( url: "https://private.example.test/redirected", method: "GET", }, + redirectResponse: { + url, + status: 302, + mimeType: "text/plain", + encodedDataLength: 23, + headers: { "X-Private": "redirect-header-secret" }, + }, }); event(socket, "Network.responseReceived", command.sessionId, { requestId: "request-1", response: { url: - options.redirectToDisallowedOrigin === true + options.responseAfterMalformedUrl ?? + (options.redirectToDisallowedOrigin === true ? "https://private.example.test/redirected" - : url, + : responseUrl), status: 200, mimeType: "application/json", headers: { diff --git a/tests/fixtures/fakeCdpBrowserTypes.ts b/tests/fixtures/fakeCdpBrowserTypes.ts index 17d3452cc7..27bab0dc3a 100644 --- a/tests/fixtures/fakeCdpBrowserTypes.ts +++ b/tests/fixtures/fakeCdpBrowserTypes.ts @@ -75,6 +75,12 @@ export interface FakeOptions { readonly indexedDbDateKeys?: boolean; readonly foreignSessionEvents?: boolean; readonly redirectToDisallowedOrigin?: boolean; + readonly redirectFromDisallowedOrigin?: boolean; + readonly redirectWithinOrigin?: boolean; + readonly malformedRedirectResponse?: boolean; + readonly redirectResponseUrl?: string; + readonly redirectResponseEnvelope?: unknown; + readonly responseAfterMalformedUrl?: string; readonly unrelatedWorker?: boolean; readonly binaryWebSocketEvent?: boolean; readonly invalidBinaryWebSocketEvent?: boolean; diff --git a/tests/fixtures/fakeGhidra.mjs b/tests/fixtures/fakeGhidra.mjs index d0b9655f9b..5cc0f2c169 100644 --- a/tests/fixtures/fakeGhidra.mjs +++ b/tests/fixtures/fakeGhidra.mjs @@ -108,11 +108,22 @@ const handleRequest = (socket, server, request, state) => { if (state.mode === "exit_tools" && request.method !== "shutdown") process.exit(74); if (state.mode === "hang_tools" && request.method !== "shutdown") return; - const inventory = inventoryResultFor(request.method); + const inventory = + request.method === "list_strings" && state.mode === "fragmented" + ? [stringItem("0x2000", "fixture 雪🦊")] + : request.method === "list_strings" && state.mode === "oversized_result" + ? [stringItem("0x2000", "x".repeat(64 * 1024 * 1024 + 1))] + : inventoryResultFor(request.method); if (inventory !== undefined) { - socket.write( - `${JSON.stringify({ id: request.id, ok: true, result: inventory })}\n`, - ); + const response = `${JSON.stringify({ id: request.id, ok: true, result: inventory })}\n`; + if (state.mode === "fragmented") { + const bytes = Buffer.from(response, "utf8"); + const unicodeOffset = bytes.indexOf(Buffer.from("雪", "utf8")); + const midpoint = + unicodeOffset >= 0 ? unicodeOffset + 1 : Math.floor(bytes.length / 2); + socket.write(bytes.subarray(0, midpoint)); + setTimeout(() => socket.write(bytes.subarray(midpoint)), 5); + } else socket.write(response); return; } if (request.method === "shutdown") { @@ -304,7 +315,7 @@ const stringItem = (address, value) => ({ string: { encoding: "UTF-8", termination: "present_or_not_required", - byte_length: value.length + 1, + byte_length: Buffer.byteLength(value, "utf8") + 1, }, }); diff --git a/tests/fixtures/fakeHopper.mjs b/tests/fixtures/fakeHopper.mjs index 07cc6bddd7..a65073b35e 100644 --- a/tests/fixtures/fakeHopper.mjs +++ b/tests/fixtures/fakeHopper.mjs @@ -135,7 +135,10 @@ const sendEchoWithProgress = (send, request) => { terminal: true, }, }); - send({ id: request.id, result: request.params ?? {} }); + send( + { id: request.id, result: request.params ?? {} }, + request.params?.fragmented === true, + ); }; if (typeof request.params?.gate === "string") heldReplies.set(request.id, reply); @@ -149,9 +152,12 @@ const server = createServer((socket) => { const send = (message, fragmented = false) => { const line = `${JSON.stringify(message)}\n`; if (!fragmented) return socket.write(line); - const midpoint = Math.floor(line.length / 2); - socket.write(line.slice(0, midpoint)); - setTimeout(() => socket.write(line.slice(midpoint)), 2); + const bytes = Buffer.from(line, "utf8"); + const unicodeOffset = bytes.indexOf(Buffer.from("雪", "utf8")); + const midpoint = + unicodeOffset >= 0 ? unicodeOffset + 1 : Math.floor(bytes.length / 2); + socket.write(bytes.subarray(0, midpoint)); + setTimeout(() => socket.write(bytes.subarray(midpoint)), 2); }; socket.on("data", (chunk) => { buffer += chunk; @@ -187,6 +193,9 @@ const server = createServer((socket) => { if (shutdownMode !== "cleanup-required") setTimeout(closeServer, 2); } else if (request.method === "hang") { // Deliberately leave the request pending. + } else if (request.method === "partial_then_wait") { + socket.write(`{"id":${request.id},"result":"stale`); + process.send?.({ id: request.id, method: "fixture_partial_write" }); } else if (request.method === "exit") { process.exit(7); } else if (request.method === "malformed") { diff --git a/tests/fixtures/filesystemSnapshotFifoProbe.mjs b/tests/fixtures/filesystemSnapshotFifoProbe.mjs new file mode 100644 index 0000000000..de3e466b3d --- /dev/null +++ b/tests/fixtures/filesystemSnapshotFifoProbe.mjs @@ -0,0 +1,23 @@ +import { execFileSync } from "node:child_process"; +import { lstat, unlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const [snapshotModulePath, directory] = process.argv.slice(2); +if (snapshotModulePath === undefined || directory === undefined) + throw new Error( + "Filesystem snapshot FIFO probe requires a module path and directory", + ); + +const { hashFile } = await import(pathToFileURL(snapshotModulePath)); +const path = join(directory, "observed-file"); +await writeFile(path, "regular file\n"); +const expected = await lstat(path); +await unlink(path); +execFileSync("mkfifo", [path]); +// This invokes the production lstat-to-open hashing seam with a known captured +// regular-file identity. The owner test bounds this child so an O_RDONLY FIFO +// regression cannot hang Vitest. +const digest = await hashFile(path, expected, 1_000); +if (digest !== null) throw new Error("Expected the FIFO to be rejected"); +process.stdout.write("rejected\n"); diff --git a/tests/fixtures/historicalHar.ts b/tests/fixtures/historicalHar.ts new file mode 100644 index 0000000000..1c25421054 --- /dev/null +++ b/tests/fixtures/historicalHar.ts @@ -0,0 +1,36 @@ +/** Synthetic HAR 1.2 fixture with producer-declared sizes independent of Unicode content. */ +export const historicalHar = () => ({ + log: { + version: "1.2", + creator: { name: "rea-source-owned-fixture", version: "1" }, + entries: [ + { + startedDateTime: "2026-10-07T13:00:00.123+08:00", + time: 1.25, + request: { + method: "GET", + url: "https://example.test/a?token=ordinary#fragment", + httpVersion: "HTTP/2", + cookies: [], + headers: [], + queryString: [], + headersSize: -1, + bodySize: 0, + }, + response: { + status: 200, + statusText: "OK", + httpVersion: "HTTP/2", + cookies: [], + headers: [], + content: { size: 99, mimeType: "text/plain", text: "雪😀\r\n" }, + redirectURL: "", + headersSize: -1, + bodySize: 99, + }, + cache: {}, + timings: { send: 0, wait: 1.25, receive: 0 }, + }, + ], + }, +}); diff --git a/tests/fixtures/hopperCleanupTimeout.py b/tests/fixtures/hopperCleanupTimeout.py new file mode 100644 index 0000000000..1a3d76c179 --- /dev/null +++ b/tests/fixtures/hopperCleanupTimeout.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import importlib.util +import json +import subprocess +import sys +import time + +helper_path = sys.argv[1] +spec = importlib.util.spec_from_file_location("hopper_demo_x11", helper_path) +if spec is None or spec.loader is None: + raise SystemExit("unable to load helper") +helper = importlib.util.module_from_spec(spec) +spec.loader.exec_module(helper) + +child = subprocess.Popen( + [ + sys.executable, + "-c", + ( + "import signal,time;" + "signal.signal(signal.SIGTERM, signal.SIG_IGN);" + "print('ready', flush=True);" + "time.sleep(100)" + ), + ], + stdout=subprocess.PIPE, + text=True, +) +assert child.stdout is not None +if child.stdout.readline().strip() != "ready": + raise SystemExit("child did not become ready") + +started = time.monotonic() +helper.stop_process(child) +elapsed = time.monotonic() - started + +print( + json.dumps( + { + "returncode": child.returncode, + "elapsed_ms": round(elapsed * 1000), + }, + separators=(",", ":"), + ) +) + +if child.returncode != -9: + raise SystemExit(f"expected SIGKILL fallback, got {child.returncode}") +if not 1800 <= elapsed * 1000 < 4000: + raise SystemExit(f"unexpected cleanup duration: {elapsed}") diff --git a/tests/fixtures/hopperDialogDetection.py b/tests/fixtures/hopperDialogDetection.py new file mode 100644 index 0000000000..b3a44ad3a6 --- /dev/null +++ b/tests/fixtures/hopperDialogDetection.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +import importlib.util +import json +import sys + +helper_path = sys.argv[1] +spec = importlib.util.spec_from_file_location("hopper_demo_x11", helper_path) +if spec is None or spec.loader is None: + raise SystemExit("unable to load helper") +helper = importlib.util.module_from_spec(spec) +spec.loader.exec_module(helper) + + +class FakeX11: + def __init__(self, titles, transients): + self.titles = titles + self.transients = transients + + def XFetchName(self, _display, window, target): + value = self.titles.get(window) + if value is None: + return 0 + target._obj.value = value.encode("utf-8") + return 1 + + def XGetTransientForHint(self, _display, window, target): + value = self.transients.get(window) + if value is None: + return 0 + target._obj.value = value + return 1 + + def XFree(self, _value): + return 1 + + +x11 = FakeX11( + { + 10: "Registration", + 11: "Hopper Disassembler", + 20: "Registration", + 21: "Unrelated application", + 30: "Other window", + }, + { + 10: 11, + 20: 21, + }, +) + +observed = [ + (20, 189, 370, 901, 284), + (30, 0, 0, 1280, 1024), + (10, 20, 100, 1200, 284), +] + +dialog = helper.find_demo_dialog(x11, 0, observed) +if dialog is None: + raise SystemExit("expected Hopper registration dialog was not detected") +if dialog[0] != 10: + raise SystemExit(f"wrong dialog detected: {dialog!r}") + +print( + json.dumps( + { + "dialog": dialog, + "click": helper.demo_click(dialog), + "wrong_parent_rejected": helper.find_demo_dialog( + x11, + 0, + [observed[0], observed[1]], + ) + is None, + }, + separators=(",", ":"), + ) +) diff --git a/tests/fixtures/processFidelity.mjs b/tests/fixtures/processFidelity.mjs index 83f1303593..bac307179c 100644 --- a/tests/fixtures/processFidelity.mjs +++ b/tests/fixtures/processFidelity.mjs @@ -1,4 +1,5 @@ import { spawn } from "node:child_process"; +import { writeFile } from "node:fs/promises"; const mode = process.argv[2]; @@ -63,6 +64,32 @@ if (mode === "interactive") { process.exit(23); } else if (mode === "hang") { setInterval(() => undefined, 1_000); +} else if (mode === "ready-hang") { + await writeFile(process.argv[3], "ready"); + process.stdout.write("ready\n"); + setInterval(() => undefined, 1_000); +} else if (mode === "detached-cleanup") { + const owned = spawn(process.execPath, [process.argv[1], "detached-child"], { + detached: true, + stdio: "ignore", + }); + const unowned = spawn(process.execPath, [process.argv[1], "detached-child"], { + detached: true, + env: { ...process.env, REA_PROCESS_RUN_ID: "unowned-neighbor" }, + stdio: "ignore", + }); + if (owned.pid === undefined || unowned.pid === undefined) + throw new Error("detached fixture children did not receive PIDs"); + owned.unref(); + unowned.unref(); + await writeFile( + process.argv[3], + JSON.stringify({ ownedPid: owned.pid, unownedPid: unowned.pid }), + ); + await new Promise((resolve) => setTimeout(resolve, 100)); + process.stdout.write("detached-ready\n"); +} else if (mode === "detached-child") { + setInterval(() => undefined, 1_000); } else { process.stderr.write("unknown fixture mode\n"); process.exit(2); diff --git a/tests/fixtures/processRunTokenParserProbe.swift b/tests/fixtures/processRunTokenParserProbe.swift new file mode 100644 index 0000000000..b7185d4337 --- /dev/null +++ b/tests/fixtures/processRunTokenParserProbe.swift @@ -0,0 +1,176 @@ +import Foundation + +func fixture( + arguments: [String], + environment: [String], + pointerSize: Int = 8, + executablePath: String = "/fixture/executable", + appleRecords: [String] = ["pfz=synthetic"], + zeroedPfz: Bool = false +) -> [UInt8] { + var bytes = withUnsafeBytes(of: Int32(arguments.count).littleEndian, Array.init) + let pathBytes = Array(executablePath.utf8) + bytes.append(contentsOf: pathBytes + [0]) + let prefixLength = Array("executable_path=".utf8).count + let padding = (pointerSize - ((prefixLength + pathBytes.count + 1) % pointerSize)) % pointerSize + bytes.append(contentsOf: repeatElement(UInt8(0), count: padding)) + for argument in arguments { bytes.append(contentsOf: Array(argument.utf8) + [0]) } + for entry in environment { bytes.append(contentsOf: Array(entry.utf8) + [0]) } + if zeroedPfz { + bytes.append(contentsOf: repeatElement(UInt8(0), count: Array("pfz=synthetic".utf8).count + 1)) + } + for record in appleRecords { bytes.append(contentsOf: Array(record.utf8) + [0]) } + return bytes +} + +@main +struct ProcessRunTokenParserProbe { + static func main() throws { + let argvDecoy = try token(in: fixture( + arguments: ["/fixture/executable", "REA_PROCESS_RUN_ID=argv-decoy", "trailing"], + environment: ["DECOY=words REA_PROCESS_RUN_ID=synthetic-decoy"] + )) + let realToken = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["DECOY=words REA_PROCESS_RUN_ID=synthetic-decoy", "REA_PROCESS_RUN_ID=owned-token"] + )) + let emptyLaterArgument = try token(in: fixture( + arguments: ["/fixture/executable", "", "after-empty"], + environment: ["REA_PROCESS_RUN_ID=empty-argument-token"], + pointerSize: 8 + )) + let alternatePadding = try token(in: fixture( + arguments: ["/fixture/executable", "argument"], + environment: ["REA_PROCESS_RUN_ID=alternate-padding-token"], + pointerSize: 4 + ), pointerSize: 4) + let emptyArgv0Read = try token(in: fixture( + arguments: ["", "", "after-empty"], + environment: ["REA_PROCESS_RUN_ID=empty-argv0-token"], + pointerSize: 8 + )) + let finalAssignmentArgumentRead = try token(in: fixture( + arguments: ["/fixture/executable", "FOO=bar"], + environment: ["REA_PROCESS_RUN_ID=final-assignment-token"] + )) + let reservedArgumentIgnored = try token(in: fixture( + arguments: ["/fixture/executable", "REA_PROCESS_RUN_ID=argv-decoy"], + environment: ["DECOY=words REA_PROCESS_RUN_ID=synthetic-decoy"] + )) + let nonAsciiPathRead = try token(in: fixture( + arguments: ["/fixture/é", "FOO=bar"], + environment: ["REA_PROCESS_RUN_ID=utf8-path-token"], + pointerSize: 8, + executablePath: "/fixture/é" + )) + let appleVectorTokenFailsClosed: Bool + do { + _ = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["pfz=user-value", "PATH=/usr/bin"], + appleRecords: ["pfz=synthetic", "REA_PROCESS_RUN_ID=apple-decoy"] + )) + appleVectorTokenFailsClosed = false + } catch RunTokenReadError.ambiguousEnvironmentBoundary { + appleVectorTokenFailsClosed = true + } + let clearedAppleVectorTokenFailsClosed: Bool + do { + _ = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["REA_PROCESS_RUN_ID=cleared-pfz-owned-token"], + appleRecords: ["REA_PROCESS_RUN_ID=apple-decoy"], + zeroedPfz: true + )) + clearedAppleVectorTokenFailsClosed = false + } catch RunTokenReadError.ambiguousEnvironmentBoundary { + clearedAppleVectorTokenFailsClosed = true + } + let callerPfzBeforeTokenRead = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["pfz=caller-value", "REA_PROCESS_RUN_ID=cleared-pfz-owned-token"], + zeroedPfz: true + )) + let emptyEnvironmentRecordsBeforeTokenFailClosed: Bool + do { + _ = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["DECOY=value", "", "", "REA_PROCESS_RUN_ID=synthetic-decoy"], + appleRecords: [], + zeroedPfz: true + )) + emptyEnvironmentRecordsBeforeTokenFailClosed = false + } catch RunTokenReadError.ambiguousEnvironmentBoundary { + emptyEnvironmentRecordsBeforeTokenFailClosed = true + } + let zeroArgumentsRead = try token(in: fixture( + arguments: [], + environment: ["REA_PROCESS_RUN_ID=zero-argc-token"], + pointerSize: 4 + ), pointerSize: 4) + var truncatedArgv = withUnsafeBytes(of: Int32(2).littleEndian, Array.init) + let truncatedPath = Array("/fixture/executable".utf8) + truncatedArgv.append(contentsOf: truncatedPath + [0]) + let truncatedPadding = (8 - ((Array("executable_path=".utf8).count + truncatedPath.count + 1) % 8)) % 8 + truncatedArgv.append(contentsOf: repeatElement(UInt8(0), count: truncatedPadding)) + truncatedArgv.append(contentsOf: Array("/fixture/executable".utf8) + [0]) + truncatedArgv.append(contentsOf: Array("REA_PROCESS_RUN_ID=synthetic-decoy".utf8)) + let truncatedArgvFailsClosed: Bool + do { + _ = try token(in: truncatedArgv) + truncatedArgvFailsClosed = false + } catch { + truncatedArgvFailsClosed = true + } + let duplicateTokenFailsClosed: Bool + do { + _ = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["REA_PROCESS_RUN_ID=first", "REA_PROCESS_RUN_ID=second"] + )) + duplicateTokenFailsClosed = false + } catch RunTokenReadError.duplicateToken { + duplicateTokenFailsClosed = true + } + let emptyEnvironmentFailsClosed: Bool + do { + _ = try token(in: fixture(arguments: ["/fixture/executable"], environment: [])) + emptyEnvironmentFailsClosed = false + } catch RunTokenReadError.environmentUnavailable { + emptyEnvironmentFailsClosed = true + } + let missingAppleBoundaryFailsClosed: Bool + do { + _ = try token(in: fixture( + arguments: ["/fixture/executable"], + environment: ["REA_PROCESS_RUN_ID=synthetic-decoy"], + appleRecords: [] + )) + missingAppleBoundaryFailsClosed = false + } catch RunTokenReadError.appleVectorUnavailable { + missingAppleBoundaryFailsClosed = true + } + + let result: [String: Any] = [ + "argvDecoyIgnored": argvDecoy == nil, + "realTokenRead": realToken == "owned-token", + "emptyLaterArgumentRead": emptyLaterArgument == "empty-argument-token", + "alternatePaddingRead": alternatePadding == "alternate-padding-token", + "truncatedArgvFailsClosed": truncatedArgvFailsClosed, + "duplicateTokenFailsClosed": duplicateTokenFailsClosed, + "emptyEnvironmentFailsClosed": emptyEnvironmentFailsClosed, + "missingAppleBoundaryFailsClosed": missingAppleBoundaryFailsClosed, + "emptyArgv0Read": emptyArgv0Read == "empty-argv0-token", + "finalAssignmentArgumentRead": finalAssignmentArgumentRead == "final-assignment-token", + "reservedArgumentIgnored": reservedArgumentIgnored == nil, + "nonAsciiPathRead": nonAsciiPathRead == "utf8-path-token", + "appleVectorTokenFailsClosed": appleVectorTokenFailsClosed, + "clearedAppleVectorTokenFailsClosed": clearedAppleVectorTokenFailsClosed, + "callerPfzBeforeTokenRead": callerPfzBeforeTokenRead == "cleared-pfz-owned-token", + "emptyEnvironmentRecordsBeforeTokenFailClosed": emptyEnvironmentRecordsBeforeTokenFailClosed, + "zeroArgumentsRead": zeroArgumentsRead == "zero-argc-token", + ] + let output = try JSONSerialization.data(withJSONObject: result, options: [.sortedKeys]) + FileHandle.standardOutput.write(output) + } +} diff --git a/tests/fixtures/processRunTokenSnapshotProbe.swift b/tests/fixtures/processRunTokenSnapshotProbe.swift new file mode 100644 index 0000000000..bfb2aae7c7 --- /dev/null +++ b/tests/fixtures/processRunTokenSnapshotProbe.swift @@ -0,0 +1,40 @@ +import Darwin +import Foundation + +@main +struct ProcessRunTokenSnapshotProbe { + static func main() throws { + var sizeQueries = 0 + var readAttempts = 0 + let retried = readProcessSnapshot { _, output, size in + guard output != nil else { + sizeQueries += 1 + size = MemoryLayout.stride + return 0 + } + readAttempts += 1 + if readAttempts == 1 { + errno = ENOMEM + return -1 + } + size = 0 + return 0 + } + + let otherError = readProcessSnapshot { _, output, size in + if output == nil { + size = MemoryLayout.stride + return 0 + } + errno = EACCES + return -1 + } + + let output: [String: Any] = [ + "retriedAfterFreshSize": retried.failure == nil && sizeQueries == 2 && readAttempts == 2, + "preservedOtherErrno": otherError.failure == "process_table_failed_\(EACCES)" + ] + let data = try JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) + FileHandle.standardOutput.write(data) + } +} diff --git a/tests/fixtures/processSnapshotCancellation.mjs b/tests/fixtures/processSnapshotCancellation.mjs new file mode 100644 index 0000000000..6da752d822 --- /dev/null +++ b/tests/fixtures/processSnapshotCancellation.mjs @@ -0,0 +1,8 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const [root] = process.argv.slice(2); +if (root === undefined) + throw new Error("Snapshot cancellation fixture requires an observation root"); + +await writeFile(join(root, "created-after-start"), "x"); diff --git a/tests/process-global/cliJsonInputStatus.test.ts b/tests/process-global/cliJsonInputStatus.test.ts index 582bb59463..d69dcbc614 100644 --- a/tests/process-global/cliJsonInputStatus.test.ts +++ b/tests/process-global/cliJsonInputStatus.test.ts @@ -11,43 +11,31 @@ afterEach(() => { }); describe("CLI JSON input logging status", () => { - it.each([ - {}, - { input_path: "input.json", input_reason: "invalid-json" }, - { input_path: "missing.json", input_reason: "read-failed" }, - { input_path: "", input_reason: "read-failed" }, - ])( - "preserves failure diagnostics and logs an error for %j", - async (metadata) => { - const parsed = await parseCliJsonInput("{", "test-input"); - if (parsed.ok) throw new Error("Expected malformed inline JSON to fail"); - const value = Object.assign({}, parsed.error, metadata); - const serialized = JSON.stringify(value); - const lines: string[] = []; - const logger = pino( - { level: "info" }, - { write: (line) => lines.push(line) }, - ); - process.exitCode = undefined; + it("preserves parsed failure diagnostics and logs an error", async () => { + const parsed = await parseCliJsonInput("{", "test-input"); + if (parsed.ok) throw new Error("Expected malformed inline JSON to fail"); + const value = parsed.error; + const serialized = JSON.stringify(value); + const lines: string[] = []; + const logger = pino( + { level: "info" }, + { write: (line) => lines.push(line) }, + ); + process.exitCode = undefined; - const output = await logCliCommand( - logger, - "test-input", - async () => value, - ); + const output = await logCliCommand(logger, "test-input", async () => value); - expect(output).toBe(value); - expect(JSON.stringify(output)).toBe(serialized); - expect(process.exitCode).toBe(1); - expect(lines).toHaveLength(1); - expect(JSON.parse(lines.join(""))).toMatchObject({ - level: 50, - command: "test-input", - status: "error", - msg: "CLI command failed", - }); - }, - ); + expect(output).toBe(value); + expect(JSON.stringify(output)).toBe(serialized); + expect(process.exitCode).toBe(1); + expect(lines).toHaveLength(1); + expect(JSON.parse(lines.join(""))).toMatchObject({ + level: 50, + command: "test-input", + status: "error", + msg: "CLI command failed", + }); + }); it("leaves ordinary parsed JSON successful and unchanged", async () => { const parsed = await parseCliJsonInput( diff --git a/tests/process-global/verifierRun.test.ts b/tests/process-global/verifierRun.test.ts index 3b42e282dc..10cb80f649 100644 --- a/tests/process-global/verifierRun.test.ts +++ b/tests/process-global/verifierRun.test.ts @@ -104,26 +104,6 @@ describe("verifier run identity", { concurrent: false }, () => { } }, ); - - it.runIf(process.platform !== "win32")( - "bounds cleanup when a verifier child ignores SIGTERM", - async () => { - const child = spawn( - process.execPath, - [ - "-e", - "process.on('SIGTERM', () => {}); process.stdout.write('ready'); setInterval(() => {}, 1000)", - ], - { stdio: ["ignore", "pipe", "ignore"] }, - ); - try { - await childReady(child); - } finally { - await stopChild(child); - } - expect(child.signalCode).toBe("SIGKILL"); - }, - ); }); const spawnReadyChild = (env: NodeJS.ProcessEnv): ChildProcess => diff --git a/tests/process-global/vitestConfiguration.test.ts b/tests/process-global/vitestConfiguration.test.ts deleted file mode 100644 index 33dee06ff0..0000000000 --- a/tests/process-global/vitestConfiguration.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { execFile } from "node:child_process"; -import { readdir } from "node:fs/promises"; -import { join, relative, resolve } from "node:path"; -import { promisify } from "node:util"; - -import { describe, expect, it } from "vitest"; - -const execute = promisify(execFile); -describe("Vitest project configuration", () => { - it("classifies every deterministic test in exactly one project", async () => { - const { stdout } = await execute( - process.execPath, - [ - resolve("node_modules/vitest/vitest.mjs"), - "list", - "--filesOnly", - "--staticParse", - ], - { cwd: process.cwd(), maxBuffer: 4 * 1_024 * 1_024 }, - ); - const classified = parseProjects(stdout); - const repositoryTests = [ - ...(await testFiles("src")), - ...(await testFiles("tests")), - ].sort(); - - expect( - [...classified.values()] - .filter((owners) => owners.length > 1) - .map((owners) => owners.join(", ")), - ).toEqual([]); - expect([...classified.keys()].sort()).toEqual(repositoryTests); - }, 20_000); -}); - -const parseProjects = (output: string): Map => { - const classified = new Map(); - for (const line of output.split("\n")) { - const trimmed = line.trim(); - const separator = trimmed.indexOf("] "); - if (!trimmed.startsWith("[") || separator < 2) continue; - const project = trimmed.slice(1, separator); - const path = trimmed.slice(separator + 2); - if (path.length === 0) continue; - const owners = classified.get(path) ?? []; - owners.push(project); - classified.set(path, owners); - } - return classified; -}; - -const testFiles = async (root: string): Promise => { - const files: string[] = []; - for (const entry of await readdir(root, { withFileTypes: true })) { - const path = join(root, entry.name); - if (entry.isDirectory()) files.push(...(await testFiles(path))); - else if (entry.isFile() && entry.name.endsWith(".test.ts")) - files.push(relative(process.cwd(), path)); - } - return files; -}; diff --git a/third_party/evmole/LICENSE b/third_party/evmole/LICENSE new file mode 100644 index 0000000000..5f31d41836 --- /dev/null +++ b/third_party/evmole/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Maxim Andreev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/evmole/README.md b/third_party/evmole/README.md new file mode 100644 index 0000000000..0b7a635150 --- /dev/null +++ b/third_party/evmole/README.md @@ -0,0 +1,22 @@ +# EVM interface upstream profile + +REA integrates unchanged [EVMole](https://github.com/cdump/evmole) as the exact +npm dependency `evmole@0.9.3`, with lockfile integrity verification. Tag `0.9.3` +resolves to commit `0409707c2efef11a9043f9b4505cad5719054a7e`. + +The JavaScript binding and WASM engine remain in the original upstream package; +REA contains only carrier validation, a worker adapter and portable contracts. +No submodule or vendored engine fork is required. No signature database, +LLM decompiler, blockchain node or RPC dependency is acquired. + +Upstream is MIT, copyright (c) 2023 Maxim Andreev. Its npm tarball omits the +license file, so REA packages the unchanged tag's [LICENSE](LICENSE) here. +The engine is initialized through the upstream `no_tla`/`initSync` interface +only inside an owned worker; importing REA services performs no WASM/file I/O. + +Initial interface analysis uses `contractInfo` with selectors, arguments and +stateMutability enabled. Selector recovery also extracts/reports terminal metadata; +its reported representation is retained with explicit unknown precision/coverage. +Other upstream outputs are separate future contracts; +in particular, CFG destination IDs are not byte addresses and terminal CBOR +metadata cannot establish a lossless integer claim through the current binding. diff --git a/third_party/pwndbg/LICENSE.md b/third_party/pwndbg/LICENSE.md new file mode 100644 index 0000000000..bbc1e5d145 --- /dev/null +++ b/third_party/pwndbg/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Dominik 'Disconnect3d' Czarnota + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/pwndbg/README.md b/third_party/pwndbg/README.md new file mode 100644 index 0000000000..e0dff52bc2 --- /dev/null +++ b/third_party/pwndbg/README.md @@ -0,0 +1,23 @@ +# pwndbg core context adapter + +REA uses caller-supplied unchanged [pwndbg](https://github.com/pwndbg/pwndbg) +2026.09.15, commit `55596aa676a2756efb8d67034f31675170eabe48`, and GDB. +The original MIT license is retained in `LICENSE.md`. Engine source and its +dependencies are not vendored or acquired by REA. + +The adapter executes the selected upstream `gdbinit.py` with its original +`__file__`, an explicitly selected existing virtual environment, and +`PWNDBG_NO_AUTOUPDATE=1`. The upstream entrypoint and dependencies are trusted +caller-supplied executable code. Reported versions establish observed behavior, +not source authenticity. The real verification lane checks out the exact commit +and uses its frozen `uv.lock` in isolated runner storage. + +Only `pwndbg.aglib.vmmap.get()` is queried, after establishing a core-only GDB +connection without an executable. All original core notes and recorded register +values come from the independent pwntools/pyelftools decoder. Mapping candidates +are derived; original pathnames do not identify current host files, and reported +flags zero leave permissions unknown. REA does not expose upstream command +execution, live attach, target launch, implicit executable pairing or updates. + +Local actual verification uses Linux x64, GDB 13.1 and the exact upstream tag/build. +Other GDB versions require their corresponding real workflow before being claimed. diff --git a/third_party/pwntools/README.md b/third_party/pwntools/README.md new file mode 100644 index 0000000000..ed9690851b --- /dev/null +++ b/third_party/pwntools/README.md @@ -0,0 +1,40 @@ +# Offline ELF upstream profile + +REA uses caller-supplied, unchanged Python packages. It vendors no pwntools, +pyelftools or Unicorn implementation and never installs these dependencies. + +| Upstream | Exact verified profile | License | Role | +| --- | --- | --- | --- | +| [pwntools](https://github.com/Gallopsled/pwntools) | 4.15.0; tag commit `3ad86ec5adab1abf9781ca7552a6f5d7cff7a33c` | MIT | ELF analysis and static mitigation/GOT/PLT heuristics | +| [pyelftools](https://github.com/eliben/pyelftools) | 0.33 | Public domain | Original ELF sections, segments, symbols and relocations | +| [Unicorn](https://github.com/unicorn-engine/unicorn) | 2.1.2 | GPL-2.0 | Upstream PLT emulation; distinct from executing the target as a host process | + +The adapter checks all three runtime versions before importing the ELF engine. +Original upstream code/licenses remain in the caller-selected environment. +No target or core binaries are committed. Fixtures are compiled in owned +temporary storage from repository source and are never executed. + +`bridge/pwntools/layout.py` is an owned allocation-failure bootstrap; +`layout_impl.py` is the adapter. It reads original tables through +unchanged upstream APIs, preserves raw name bytes/ranges, and serializes uint64 +values as strings. It never accesses `ELF.libs`, `ELF.maps`, `ELF.libc` or +`process.corefile`, which have materially different execution authority. + +The real verification lane traces host exec syscalls, checks original artifact +hashes and observes released owned processes. No negative claim relies only on +an unmodified target's sentinel: upstream can execute a patched copy when its +runtime-library accessors are used. + +The instance-local adapter subclass forwards the documented pyelftools 0.33 +`iter_segments(type=...)` / `iter_sections(type=...)` filters missing from +pwntools 4.15.0 cached overrides. No upstream file or global method is changed. +Sectionless dynamic string tables use unique DT_STRTAB/DT_STRSZ mappings to +file-backed PT_LOAD bytes; ambiguous or non-file-backed mappings are explicitly +unsupported. Section-name table escapes follow the [ELF ABI](https://gabi.xinuos.com/elf/02-eheader.html): +SHN_XINDEX represents actual indices at least 0xff00; ordinary absence uses index +zero. Raw name offsets remain reported when the name table is absent. + +PLT convenience inference uses the unchanged upstream Unicorn instruction +emulator. REA labels those maps as derived static evidence. The no-execution +verification claim concerns launching the selected object as a host process, +not the absence of static instruction emulation inside an analysis engine. diff --git a/third_party/web-network-captures/README.md b/third_party/web-network-captures/README.md new file mode 100644 index 0000000000..49d0bc1104 --- /dev/null +++ b/third_party/web-network-captures/README.md @@ -0,0 +1,36 @@ +# Historical network upstream profiles + +REA uses unmodified upstream packages or a caller-supplied tool. The adapter +code lives under `src/browser/history/` and `bridge/mitmproxy/`; this directory +records provenance rather than vendoring a second parser implementation. + +| Upstream | Fixed profile | Use | +| --- | --- | --- | +| [har-schema](https://github.com/ahmadnassri/har-schema) | npm 2.0.0, ISC | HAR 1.2 draft-06 schemas | +| [jsonc-parser](https://github.com/microsoft/node-jsonc-parser) | npm 3.3.1, MIT | Strict JSON visitor; member sequence, duplicate detection and prototype-safe materialization | +| [lossless-json](https://github.com/josdejong/lossless-json) | npm 4.3.1 | Original JSON numeric lexemes and safe-number classification | +| [AJV](https://github.com/ajv-validator/ajv) | npm 8.20.0 | Upstream schema validation | +| [ajv-formats](https://github.com/ajv-validator/ajv-formats) | npm 3.0.1 | Upstream URI, timestamp and address formats | +| [mitmproxy](https://github.com/mitmproxy/mitmproxy) | v12.2.3, commit `6c09d56e4c29a92f5ad01b03199977584b8ea14f`, MIT | Unchanged native `mitmproxy.io.tnetstring.load`; upstream `FlowWriter` / `SaveHar` for ephemeral verification fixtures | + +The npm lockfile pins exact package versions and distribution integrity hashes; +their original code and license files remain in the installed packages. Native +mitmproxy is bring-your-own. REA never installs or upgrades it and rejects a +different runtime version. The Linux verification lane downloads the official +standalone release into runner temporary storage with fixed archive SHA-256 +`2e95286b618fa6fd33e5e62a78c2e5112571d85f42ec2bac29b97ee242bdb5c5`. +This is a pinned archive digest, not a publisher signature claim. + +No native captures or game/application binaries are checked in. Verification +uses source-owned Python fixtures and upstream writers. The original file +digest/record representation is preserved independently of the decoded view; +upstream conversion defaults are not imported into native states. + +The observed `SaveHar` 12.2.3 optional `postData.text: null` quirk is admitted +only in a validation copy for that declared producer profile, with the original +null retained. The upstream schemas themselves remain unchanged. + +The strict JSON visitor supplies member events before object materialization; +REA rejects all duplicate names and retains original numeric token spelling. +The shared JSON schema currently drops `__proto__` members, so both adapters +report that representation as unsupported instead of returning incomplete data. diff --git a/turbo.json b/turbo.json index 62e4f0adeb..6fcbf9fb13 100644 --- a/turbo.json +++ b/turbo.json @@ -8,18 +8,35 @@ "package-lock.json", "scripts/check-dependency-install.mjs", "scripts/clean-build-output.mjs", + "scripts/generate-mcp-tool-catalog.mjs", "scripts/generate-package-metadata.mjs", - "scripts/lib/generated-file.mjs", - "skills/reverse-engineer-anything/SKILL.md", + "scripts/generate-skill-metadata.mjs", + "scripts/generate-product-catalog.mjs", + "scripts/generate-completion-ledger.mjs", + "scripts/lib/**/*.mjs", + "scripts/verify/managed/**", + "scripts/fixtures/managed/**", + ".oxfmtrc.json", + "skill-src/**", "src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.fixture.ts", + "!src/generatedMcpToolCatalog.ts", "tsconfig.json", "tsconfig.build.json" ], - "outputs": ["dist/**", ".cache/tsconfig.build.tsbuildinfo"] + "outputs": [ + "dist/**", + ".cache/tsconfig.build.tsbuildinfo", + "src/generatedMcpToolCatalog.ts", + "skills/**", + "docs/public/product-catalog.json", + "docs/verification/managed-conformance-manifest.json", + "docs/verification/managed-conformance-ledger.json" + ] }, "typecheck": { + "dependsOn": ["build"], "inputs": [ ".nvmrc", "package.json", @@ -39,7 +56,7 @@ "package-lock.json", "scripts/generate-package-metadata.mjs", "scripts/lib/generated-file.mjs", - "skills/reverse-engineer-anything/SKILL.md", + "skill-src/reverse-engineer-anything/SKILL.md", "src/generatedPackageMetadata.ts" ] }, @@ -57,6 +74,7 @@ }, "format:check": {}, "knip": { + "dependsOn": ["build"], "inputs": [ ".nvmrc", "knip.json", @@ -116,6 +134,7 @@ "src/generated*.ts", "src/contracts/**/*.ts", "docs/**", + "skill-src/**", "skills/**/SKILL.md", "README.md" ] diff --git a/vitest.config.ts b/vitest.config.ts index f882022154..0ab5bbc44b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -37,7 +37,7 @@ const TEST_PROJECTS = [ name: "adapters", include: [ "src/*.test.ts", - "src/{artifacts,browser,dotnet,firmware,ghidra,hopper,ida,inspector,javascript,native,process,reference,server,windows}/**/*.test.ts", + "src/{artifacts,browser,dotnet,evm,firmware,ghidra,hopper,ida,inspector,javascript,native,process,reference,server,windows}/**/*.test.ts", ], pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS, diff --git a/website/AGENTS.md b/website/AGENTS.md new file mode 100644 index 0000000000..a4bf9b8dc5 --- /dev/null +++ b/website/AGENTS.md @@ -0,0 +1,23 @@ +# Website maintenance + +> A good website is like a good paper: easy to follow, clear and concise, with a clean, refined presentation. +> +> — N0zoM1z0 + +Read [README.md](README.md) and [style-guide.md](style-guide.md) before changing +website content, layout or figures. Use [figures.md](figures.md) and `evidence/` +for asset notes and the source of case-study claims. + +- Write English, direct headings and concrete instructions. Each paragraph + should explain a new fact or action. +- Reuse the shared styles, prompts, figures and code-step components. Keep a + case study focused on one question, with further examples in native details. +- Show what REA returns and how the agent uses it. Preserve source attribution, + target identity and verification scope. +- Keep personal paths, account data, credentials and raw captures outside the + public assets. Use generic inputs and label shortened display paths. +- Check both local root and `/rea/` paths, mobile layouts, expanded details, + copying and downloads. Run `scripts/prepare-website.py` followed by + `scripts/verify-website.py`. +- Keep `website-pages.yml` as the sole, manual Pages publisher. Follow the + publication procedure in the website README. diff --git a/website/README.md b/website/README.md index 515e772383..23fbca0526 100644 --- a/website/README.md +++ b/website/README.md @@ -1,10 +1,17 @@ # REA website -An English static website with explanatory figures, worked guides and DX-Ball, Notion and TH04 investigations. +> A good website is like a good paper: easy to follow, clear and concise, with a clean, refined presentation. +> +> — N0zoM1z0 + +An English static website with explanatory figures, worked guides and DX-Ball, Notion, TH04 and CTF investigations. The public files are in `website/public/`. The site uses HTML, CSS and a small script for copying code and following the assembly-to-C comparison. Python packages the downloadable example; there is no frontend bundler or npm dependency. +[style-guide.md](style-guide.md) explains the writing, page structure, figures, +visual system and review process. Read it before adding or revising a page. + ## Local preview From the repository root: @@ -23,7 +30,9 @@ Open . Refresh the browser after editing a file. - `public/showcase/dx-ball/index.html`: sound-pan investigation and project status. - `public/showcase/notion/index.html`: Notion's Electron clipboard bridge and rich clipboard format. - `public/showcase/th04/index.html`: TH04's 16-bit DOS bullet-angle calculation and compiler checks. +- `public/showcase/ctf/index.html`: DownUnderCTF's masked-squares flag checker, extracted equations and real process captures. - `public/get-started/index.html`: agent setup, first CLI result and provider guides. +- `public/faq/index.html`: concise answers about setup, updates, analysis and troubleshooting. - `public/guides/`: a guide hub and native, JavaScript/Electron and browser examples. - `public/examples/`: downloadable Electron source and an interactive Notes browser app. - `public/assets/`: shared styles, interactions, favicon and explanatory figures. @@ -57,6 +66,18 @@ respecting reduced-motion preferences. Both the prompt and animation work without JavaScript. The homepage and agent setup section share a copyable installation prompt; setup still presents its plan for approval. +Reading pages share a small `↑ Top` link at the bottom right. It appears after +scrolling and returns to the page header, with smooth scrolling when reduced +motion is disabled. Keyboard activation returns focus to the first navigation +link. Without JavaScript, the link stays visible and uses its `#top` anchor. + +The FAQ uses native `
` for its answers. Keep replies short and link to +on-site guides for the next step. Individual answers have stable fragment IDs; +the shared script opens an answer when its fragment is visited. The questions +and disclosure controls remain usable without JavaScript. Check FAQ commands +and support statements against the English README and relevant guides when +updating them. + DX-Ball figures and findings refer to the linked 7 October 2026 checkpoint, commit `a55dca27ec0a07018c1b2c95ae2be027f7d8c3c4`. Update those links and figures together when moving to another checkpoint. Case-study source excerpts come @@ -78,7 +99,8 @@ code; the scoped lint override admits `require` only in that example directory. The Notion case follows the same HTML figure style. Its short excerpts explain the packaged clipboard bridge; separate web-cache probes illustrate the rich -clipboard and Markdown formats. Only selected source details and generic +clipboard format, with Markdown tables available as an additional example in +collapsed details. Only selected source details and generic example data belong on the site. Machine paths, account identifiers, local configuration, complete vendor bundles and raw captured results stay outside the website. @@ -94,6 +116,20 @@ records the fresh load-image/function evidence and separately credited TH04 source and historical compiler replay. The figure illustrates the calculation; original game assets and executable bytes are not website downloads. +## CTF case study + +The DownUnderCTF 2023 case follows the official `ms_flag_checker` handout from +prompt references to checking functions, compressed masks, equations and an +accepted flag. [evidence/ctf-masked-squares.md](evidence/ctf-masked-squares.md) +records the REA analysis, extracted data and positive/negative process captures. +The downloadable `public/showcase/ctf/solve.py` uses Python and `z3-solver`; +its constants come from REA's byte reads. The original executable stays in the +organizers' repository and is linked from the page. + +The mask SVG is drawn from the seventh decoded mask. It selects zero-based +position 21 and compares its character code with 55 (`7`). Keep the figure, +solver constants and evidence notes aligned when changing this case. + ## GitHub Pages `.github/workflows/website-pages.yml` prepares and deploys only `website/public`. diff --git a/website/evidence/ctf-masked-squares.md b/website/evidence/ctf-masked-squares.md new file mode 100644 index 0000000000..82f126be35 --- /dev/null +++ b/website/evidence/ctf-masked-squares.md @@ -0,0 +1,110 @@ +# DownUnderCTF masked-squares evidence + +Fresh inspection and process captures on 8 October 2026, using the published +REA 4.1.0 package with Ghidra 12.1.4 on Linux x64. The challenge is +**masked squares flag checker**, by joseph, tagged easy in the +[official DownUnderCTF 2023 repository](https://github.com/DownUnderCTF/Challenges_2023_Public/tree/b48f83db2984bc7fa2ec7879a32d7efb1be8d333/rev/masked-squares-flag-checker). + +## Target + +- Original handout: `publish/ms_flag_checker`, stripped Linux x86-64 ELF. +- Size: 15,248 bytes. +- SHA-256: `dcd3bec4f608e11f3a12ce461100aaf5f621851ee63a1f870d7c90d4e3dd51ae`. +- Upstream revision: `b48f83db2984bc7fa2ec7879a32d7efb1be8d333`. +- Git blob: `af95d9e109b5b700b3285e7e9f3b341dc716d34a`. +- REA analysis-profile digest: + `7cb87eeb3886b20e336087d762d35caac496279cf9839dcc5e741b973627c1ba`. + +The original challenge was downloaded from GitHub's contents API and decoded +from its base64 representation. The decoded length matches the API's size; +REA's target digest matches the downloaded bytes. The byte digest was checked +again after analysis and execution. The page links to the original handout; +the executable is not copied into the website. + +The supplied AGENTS.md and website writing/design guidelines were followed in +an isolated worktree. Raw results and local target paths stay in private scratch +storage; the public page uses a shortened target name in its example request. + +## Discovery through REA + +The following requests preceded reading the organizers' source, solution or +flag metadata. Searching the challenge README established its name/difficulty, +and the handout was analyzed directly. + +| Operation | Observed result | Evidence ID | +| ----------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| `search_strings`, regex `flag\|correct\|wrong\|input` | Prompt at `0x102004`; failure at `0x10201c`; success at `0x102027` | `ev_375cf2c47d9cd666eeb5527e13863ce8d01ca86d11a6957fde79227aab1266c8` | +| `xrefs`, address `0x102004` | Reference at `0x10126d` | `ev_31fb7e370e0ce8bccdf981167f49ad9196f67ac06c88eb2c1a19e3cb367e22f3` | +| `resolve_containing_procedure`, address `0x10126d` | Function at `0x10124d`, inclusive body through `0x10134f`, 259 bytes | `ev_0627cfd459c1d9255ae1b03355987a93a0827e658a7ff40abcbf2cb1cfd8e613` | +| `analyze_function`, procedure `0x10124d` | Input conversion, two helper calls, mask/target pointers, success/failure branch | `ev_4ba6d97358da28a23dc69cb6fde01cc929f28fc14f3a3661f4c65bbb6d86ec29` | +| `analyze_function`, procedure `0x101189` | Signed-byte run-length mask expansion | `ev_9e4f339895223a21cbf5bcd008ba8b5890e2e804675a11cb286f7af19246f766` | +| `analyze_function`, procedure `0x101217` | Add input integers at selected mask positions | `ev_d89261458e3faddd85110185bcd97f0f3c87e0992edc64cb3b2a1ed44846c3e9` | +| `read_bytes`, address `0x104060`, length 144 | Target numbers followed by padding and the start of the mask table | `ev_f8a3529dd5d583b06177be5ce76ca6de36f3242ac90ab30f9a3a96159ff84e53` | +| `read_bytes`, address `0x1040e0`, length 624 | All 26 mask records, 24 bytes each | `ev_944d9664d6167bf46c72b9fce662e573ea4e017c5a8ebbc305b1d943a1623301` | + +The first 104 bytes of the target read hold the 26 little-endian integers. +The displayed `read_bytes` requests preserve the actual lengths; the page +selects the seventh entries from those complete results. + +## Interpretation + +`CMP EDI,0x24` terminates the input conversion at 36 characters. Its inner +loop groups six integer cells per row. The checking loop walks from `0x1040e0` +to `0x104350` in 24-byte steps: `0x270 / 0x18 = 26` masks. + +In the mask helper, `MOVSX` loads each byte with its sign. Positive runs write +ones, negative runs write zeros, and a zero terminates a record. All extracted +records decode to exactly 36 cells. In the summing helper, a zero mask cell +skips the addition; a nonzero cell adds the corresponding input integer. +The caller compares the returned sum against a stored target. + +The seventh record starts at `0x104170` and begins `eb 01 f2 00`, meaning +`-21, +1, -14, stop`. It selects only zero-based input position 21. +Its target at `0x104078` is `37 00 00 00`, decimal 55, the code for `7`. +The SVG is calculated from this record, with only cell 21 highlighted. + +The first mask selects 16 cells and its target is 1441. In the recovered flag, +changing position 8 from `z` (122) to `y` (121) changes that sum to 1440. + +The website's C is an analyst-written summary of the sum helper and caller, +with a flattened 36-cell loop and descriptive names. Its selected assembly +is transcribed from the returned dossiers. The mask/target addresses are +analysis-image addresses; the executable's PIE runtime load address can differ. + +## Solver and source cross-check + +The downloadable Python script embeds the REA-extracted masks and targets, +expands their run lengths and gives the 26 sums to Z3. It asks for 36 character +codes using a conventional `DUCTF{...}` prefix/suffix and an alphabet of +letters, digits, underscores and braces. Those format restrictions are analyst +assumptions, distinguished from the observed binary checks in the script and +page caption. + +The solver returned `DUCTF{ezzzpzzz_07bcda7bfe81faf43caa}` and all 26 extracted +equations match that value. A separate second-model search reached its timeout; +the completed verification concerns the returned candidate. + +After obtaining and executing the candidate, the +[organizers' source](https://github.com/DownUnderCTF/Challenges_2023_Public/blob/b48f83db2984bc7fa2ec7879a32d7efb1be8d333/rev/masked-squares-flag-checker/src/ms_flag_checker.c) +confirmed the decoding/summing interpretation, all masks and all targets. +The +[challenge metadata](https://github.com/DownUnderCTF/Challenges_2023_Public/blob/b48f83db2984bc7fa2ec7879a32d7efb1be8d333/rev/masked-squares-flag-checker/ctfcli.yaml) +records the same flag. + +## Original-program verification + +Two `capture_process_scenario` requests launched the original handout in its +scratch directory, supplied one line of input and retained terminal output. + +| Input | Output | Exit | Evidence ID | +| -------------------------------------- | ------------ | ---- | --------------------------------------------------------------------- | +| `DUCTF{ezzzpzzz_07bcda7bfe81faf43caa}` | `Correct!` | 0 | `ev_99f724502d14c798959f510d274f3de0f554a27b5df5dd6cb322acfa3e90a452` | +| `DUCTF{ezyzpzzz_07bcda7bfe81faf43caa}` | `Incorrect!` | 255 | `ev_2d164f7f4c7eb139e5df8be21c1e52dc34f0ad238aea384e407a2bd6277024cb` | + +Both captures completed without truncation. REA verified its owned process-group +cleanup and removed the capture temporary roots. Filesystem observation paths +were not requested. The native analysis session was closed afterward. + +The challenge's original code and metadata are credited to DownUnderCTF/joseph. +The page, readable C summary, equation figure and Python solver are new work +based on this inspection. diff --git a/website/evidence/guide-examples.md b/website/evidence/guide-examples.md index 70cdc859c6..476c0fbd0d 100644 --- a/website/evidence/guide-examples.md +++ b/website/evidence/guide-examples.md @@ -71,6 +71,10 @@ Full local outputs and the browser verification harness are under ## Example source digests +These digests identify the files used for the recorded analyses. The browser +page later gained the site's shared `↑ Top` navigation; its `export.js` and +`notes.json` retain the versions below. + | File | SHA-256 | | -------------------------------------- | ------------------------------------------------------------------ | | `examples/notes-electron/csv.js` | `6875aeecc1d3e6c1ff5b888a199c26b377d812d5b10e5fb4025e192a206b0018` | diff --git a/website/figures.md b/website/figures.md index ddd2336772..ff8f7bba32 100644 --- a/website/figures.md +++ b/website/figures.md @@ -1,5 +1,13 @@ # REA website figures +> A good website is like a good paper: easy to follow, clear and concise, with a clean, refined presentation. +> +> — N0zoM1z0 + +Use [style-guide.md](style-guide.md#make-figures-explain-a-relationship) when adding +or revising a figure. The prompts below are historical layout references; use +the current SVG source for published labels. + The published diagrams are SVG source assets: - `public/assets/figures/rea-investigation-flow.svg` — 1774 × 887 viewBox. @@ -106,3 +114,17 @@ Second check labels verbatim: Use enough horizontal space for clear check names and generous typography. A two-line label is fine where needed. Text constraints: render every supplied English label and number verbatim with correct spelling. No extra claims, no additional labels, no footnotes, no source code or assembly, no logos or watermarks. Keep all connectors complete and directed correctly. Large legible labels matter more than decorative density. Never say full reconstruction, exact game clone, all bytes, or automatic source recovery. + +## CTF masked-squares figure + +`public/assets/figures/ctf-mask-equation.svg` explains the seventh check in +DownUnderCTF 2023's masked-squares flag checker. The original encoded mask is +`-21, +1, -14, 0`: expand it to 36 cells, highlight zero-based cell 21 and +compare its code with the recorded target 55 (`7`). Labels and selected cells +come from the fresh REA byte reads documented in +[evidence/ctf-masked-squares.md](evidence/ctf-masked-squares.md). + +The diagram is maintained as SVG source using the shared blue/gray palette. +It illustrates one equation; the downloadable Python solver uses all 26. +Keep code selectable in HTML and use the existing scrollable figure viewport +on smaller screens. diff --git a/website/public/assets/figures/ctf-mask-equation.svg b/website/public/assets/figures/ctf-mask-equation.svg new file mode 100644 index 0000000000..9b6da9680c --- /dev/null +++ b/website/public/assets/figures/ctf-mask-equation.svg @@ -0,0 +1,23 @@ + +One check reveals one character +The seventh mask selects only position 21 in the six by six input grid. Its character code must equal 55, so the character is 7. The other masks select different positions and give further equations. + + + +One check reveals one character +The seventh check. Cell labels are input positions, starting at 0. + +Encoded mask: −21, +1, −14, 0 +Skip 21 cells. Select 1. Skip 14. Stop at 0. +000102030405060708091011121314151617181920212223242526272829303132333435 + + +Only one cell contributes to this sum +code[21] = 55 +55 → '7' +Position 21 in the flag must contain 7. + +Selected: one cell +26 masks give 26 equations. +Solve them together to find the flag. + \ No newline at end of file diff --git a/website/public/assets/figures/dx-ball-sound-pan-investigation.svg b/website/public/assets/figures/dx-ball-sound-pan-investigation.svg index 103bd751b1..b1a2a51bc1 100644 --- a/website/public/assets/figures/dx-ball-sound-pan-investigation.svg +++ b/website/public/assets/figures/dx-ball-sound-pan-investigation.svg @@ -36,11 +36,11 @@ Maintained C Brick-hit caller 20 + 30 × tile_x - Original-x86 + Original code comparison 3,205 cases - VC4.0 compiler - replay + VC4.0 build + comparison 63 matching bytes diff --git a/website/public/assets/main.js b/website/public/assets/main.js index 40a1d98083..e11e0c1614 100644 --- a/website/public/assets/main.js +++ b/website/public/assets/main.js @@ -60,3 +60,49 @@ function initializeStepComparisons() { } initializeStepComparisons(); + +function initializeFaqAnswers() { + const answers = document.querySelectorAll("[data-faq] details[id]"); + if (answers.length === 0) return; + + const revealAnswer = (fragment) => { + answers.forEach((answer) => { + if (`#${answer.id}` === fragment) answer.open = true; + }); + }; + window.addEventListener("hashchange", () => + revealAnswer(window.location.hash), + ); + document.querySelectorAll('[data-faq] a[href^="#"]').forEach((link) => { + link.addEventListener("click", () => + revealAnswer(link.getAttribute("href")), + ); + }); + revealAnswer(window.location.hash); +} + +initializeFaqAnswers(); + +function initializeBackToTop() { + const link = document.querySelector(".back-to-top"); + if (link === null) return; + + const updateVisibility = () => { + link.hidden = window.scrollY < 400; + }; + window.addEventListener("scroll", updateVisibility, { passive: true }); + updateVisibility(); + + link.addEventListener("click", (event) => { + event.preventDefault(); + const reduceMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" }); + document + .querySelector(".brand, .breadcrumb a") + ?.focus({ preventScroll: true }); + }); +} + +initializeBackToTop(); diff --git a/website/public/assets/styles.css b/website/public/assets/styles.css index ce65e9ad3b..a7694af9b7 100644 --- a/website/public/assets/styles.css +++ b/website/public/assets/styles.css @@ -99,6 +99,31 @@ section[id] { .skip-link:focus { top: 0.75rem; } +.back-to-top { + position: fixed; + right: calc(1.5rem + env(safe-area-inset-right, 0px)); + bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px)); + z-index: 5; + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-height: 44px; + padding: 0.5rem 0.85rem; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--page); + color: var(--muted); + font-size: 0.875rem; + font-weight: 600; + text-decoration: none; +} +.back-to-top[hidden] { + display: none; +} +.back-to-top:hover { + background: var(--soft); + color: var(--ink); +} .site-header { border-bottom: 1px solid var(--line); } @@ -146,6 +171,26 @@ section[id] { .navigation .nav-start { color: var(--ink); } +.faq-list { + border-top: 1px solid var(--line); +} +.faq-item { + padding-block: 1rem; + border-bottom: 1px solid var(--line); + scroll-margin-top: 2rem; +} +.faq-item summary { + cursor: pointer; + color: var(--ink); + font-weight: 600; + line-height: 1.55; +} +.faq-item[open] summary { + margin-bottom: 0.9rem; +} +.faq-item > :last-child { + margin-bottom: 0; +} .eyebrow { margin-bottom: 1.2rem; font-size: 0.875rem; @@ -689,7 +734,7 @@ pre code { } .provider { display: grid; - grid-template-columns: 105px 1fr; + grid-template-columns: 105px minmax(0, 1fr); gap: 1.3rem; padding-block: 1.2rem; border-top: 1px solid var(--line); @@ -847,7 +892,8 @@ pre code { .th04-overview { margin-bottom: 3.2rem; } -.th04-ring-figure img { +.th04-ring-figure img, +.ctf-mask-figure img { min-width: 680px; } .th04-preview img { @@ -1023,15 +1069,25 @@ pre code { } @media (max-width: 640px) { + .back-to-top { + right: calc(1rem + env(safe-area-inset-right, 0px)); + bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + } .container { width: calc(100% - 2.5rem); } .header-inner { min-height: 78px; - gap: 1rem; + flex-wrap: wrap; + justify-content: flex-start; + gap: 0.55rem; + padding-block: 0.8rem; } .navigation { - gap: 1rem; + width: 100%; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.5rem 0.25rem; font-size: 0.875rem; } .hero { @@ -1107,7 +1163,7 @@ pre code { gap: 1rem; } .provider { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); gap: 0.25rem; } .guide-entry { @@ -1150,16 +1206,6 @@ pre code { } @media (max-width: 360px) { - .header-inner { - flex-wrap: wrap; - justify-content: flex-start; - gap: 0.3rem; - padding-block: 0.65rem; - } - .navigation { - width: 100%; - justify-content: space-between; - } .stats, .case-stats { grid-template-columns: 1fr; diff --git a/website/public/examples/notes-web/index.html b/website/public/examples/notes-web/index.html index 00d2c80b89..7f420d16dd 100644 --- a/website/public/examples/notes-web/index.html +++ b/website/public/examples/notes-web/index.html @@ -7,8 +7,9 @@ + - +
+ + Top + diff --git a/website/public/faq/index.html b/website/public/faq/index.html new file mode 100644 index 0000000000..f6e24ca095 --- /dev/null +++ b/website/public/faq/index.html @@ -0,0 +1,413 @@ + + + + + + FAQ — REA + + + + + + + + + +
+
+ +

REA FAQ.

+

Setup, updates, analysis and troubleshooting.

+
+
+ +
+
+

Setup and updates

+
+
+ How do I install REA? +

+ With Node.js and npm available, run setup, choose your coding + agent and review the proposed changes. +

+
+
+ Terminal · agent setup +
+
npx rea-agents@latest setup
+
+

+ Setup registers REA's MCP connection and installs the matching + investigation skill. Restart your agent when it finishes, then + give it a target path and a question. + Follow the setup guide for + prerequisites and your first analysis. +

+
+
+ Which coding agents can use REA? +

+ Any agent that supports local MCP servers. Setup includes + Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, + Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, + Command Code and VS Code. +

+

+ Connect your coding agent. +

+
+
+ How do I update REA? +

+ REA releases frequently, including bug fixes. If you installed + the CLI with npm: +

+
+
+ Terminal · installed CLI +
+
rea update
+
+

+ Follow the setup refresh command printed by the update. If you + set up your agent through npx, run: +

+
+
+ Terminal · refresh agent setup +
+
npx rea-agents@latest setup
+
+

+ Review the changes and restart your agent. Its REA connection + pins a package version, so refreshing setup makes it use the + new release. For one-off CLI commands, use + npx -y rea-agents@latest followed by the command. +

+
+
+ + Is installing the skill from skills.sh enough? + +

+ The skill gives your agent investigation instructions. Run + REA setup to connect the MCP server and + install the matching skill, then restart your agent so it can + call the tools. +

+
+
+
+ +
+

Analysis

+
+
+ What can I analyze with REA? +

Start with the kind of software you have:

+ +

+ REA also has workflows for managed assemblies, Android APKs, + firmware and packaged resources. + See the case studies for worked + investigations. +

+
+
+ Do I need Hopper, Ghidra or IDA? +

+ Deep native analysis uses one of them. Static JavaScript and + .NET inspection work with the supplied files and do not need a + native analysis engine. +

+

+ Connect your existing Ghidra or IDA installation, or let setup + detect Hopper and propose an installation. Your host and + target determine which provider can be used. + Set up native analysis. +

+
+
+ Can I use REA without a coding agent? +

+ Yes. The CLI uses the same analysis workflows. For a local + JavaScript or Electron app: +

+
+
+ Terminal · static application analysis +
+
npx -y rea-agents@latest analyze-javascript-application \
+  /absolute/path/to/app --json
+
+

+ Replace the path with an extracted application folder or ASAR + file. Use the CLI for + installation options. +

+
+
+ What code does REA return? +

+ Native analysis returns assembly and pseudocode, with calls, + references and relevant constants. JavaScript analysis returns + modules, their relationships and source locations. +

+

+ Your agent uses these findings to explain the behavior or + write and test an implementation. The + DX-Ball calculation + and CTF flag checker show how + that process works. +

+
+
+ What should I ask my agent? +

+ Give it a local target and one behavior to investigate. For a + JavaScript or Electron app: +

+
+
+ Your coding agent +
+

+ Use REA to analyze /absolute/path/to/app and explain how + it exports data. Show the relevant code and source + locations. +

+
+

+ Replace the path with your target. The + guides provide prompts for native, + Electron and browser examples. +

+
+
+
+ +
+

Troubleshooting

+
+
+ Why can't my agent see the REA tools? +

+ Restart or reconnect your agent after setup. Check its REA + registration with doctor; for Codex: +

+
+
+ Terminal · check one client +
+
npx -y rea-agents@latest doctor --client codex --json
+
+

+ Replace codex with your client ID, such as + claude_code or cursor. If the report + shows a missing or stale registration, run setup for that + client: +

+
+
+ Terminal · repair one client's setup +
+
npx rea-agents@latest setup --client codex
+
+

+ Review its plan and restart the client. If registration is + healthy but tools remain absent, check the client's MCP + connection error. + More setup diagnostics. +

+
+
+ Do I need to start Hopper first? +

+ REA starts Hopper when an operation needs it. On macOS, + complete the first-run choice of demo mode or license + activation before unattended analysis. + See native provider setup. +

+
+
+ What should I do if I hit a bug? +

+ Update REA, restart your agent and retry + the same task. A recent release may already fix the problem. +

+

+ If it persists, + open an issue + with your REA version, operating system, target type, steps to + reproduce and error output. Include the provider name when the + problem involves native analysis. +

+
+
+
+ +
+

Data and sharing

+
+
+ Does REA upload my app? +

+ REA analyzes targets on your machine. Your agent receives the + results, and its model provider's data policy applies to those + results. +

+
+
+ How can I share an example or contribute? +

+ Have a useful REA example? We'd love to see it. Share the + target, your question, the findings and how you checked them + in an + issue + or + pull request. Short code excerpts and figures help others follow the + investigation. +

+

+ Bug reports, feature requests, documentation and code + improvements are welcome too. You can also discuss an example + in the community. +

+
+
+
+
+
+
+ +

+ Top + + diff --git a/website/public/get-started/index.html b/website/public/get-started/index.html index 15314573f4..042c74c457 100644 --- a/website/public/get-started/index.html +++ b/website/public/get-started/index.html @@ -13,7 +13,7 @@ - +
-

Start with a question.

+

Set up REA.

Connect REA to your coding agent, or inspect a local app from your terminal. @@ -43,17 +43,17 @@

-

With your agent

+

Connect your coding agent

Run setup to connect REA to your coding agent. Client choices include Claude Code, Codex, Cursor, Gemini CLI, Windsurf and other @@ -118,19 +118,14 @@ involved, with the source locations for your findings.

-

- Replace the path with your local target. Read the findings, then - ask a narrower follow-up about a function or connection you want - to understand. -

+

Replace the path with your local target.

-

From the terminal

+

Use the CLI

- You can start with a JavaScript or Electron application folder or - ASAR archive. This path uses Node.js and npm and reads the app - without running it. + Analyze a JavaScript or Electron application folder or ASAR + archive without running the app:

@@ -153,10 +148,9 @@ "D:/apps/example" works here.

- The result includes the application's identity, recovered module - relationships and evidence locations. It also records - relationships that remain unresolved. Use those findings to choose - where to investigate next. + The result identifies the app, shows connections between modules + and gives their source locations. Connections REA cannot resolve + are marked in the result.

If you'd like a permanent rea command in your shell: @@ -183,116 +177,121 @@

-

Working with native binaries

+

Set up native analysis

- REA connects your agent to a local analysis provider. Choose the - tool you already use, configure its connection, then ask about - your executable or library. + Native binaries need a local analysis tool. Choose and configure + one below, then give REA your executable or library.

-
-
-

Ghidra

-
-

- Use an existing Ghidra 12.1.x installation and a 64-bit JDK - 21 or newer on Linux x64 or macOS x64/arm64. Set the paths - before running setup: -

-
- Connect your Ghidra installation -
-
- Ghidra connection · macOS / Linux -
-
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
+            
+ Choose a tool: Ghidra, Hopper or IDA +
+
+

Ghidra

+
+

+ Use an existing Ghidra 12.1.x installation and a 64-bit + JDK 21 or newer on Linux x64 or macOS x64/arm64. Set the + paths before running setup: +

+
+ Connect your Ghidra installation +
+
+ Ghidra connection · macOS / Linux +
+
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
 export JAVA_HOME=/absolute/path/to/jdk-21
 npx -y rea-agents@latest doctor --provider ghidra --json
 npx rea-agents@latest setup
-
-

- Replace both paths with your existing installations. Setup - checks them and records valid settings for your selected - client. On macOS, use a Ghidra package with the native - decompiler matching your host architecture. -

-
-
-
-
-

Hopper

-
-

- Connect Hopper on macOS or Linux. Run the setup command - above; it detects an existing installation or offers an - installation plan for your approval. -

-

- On macOS, complete Hopper's first-run choice of demo mode or - license activation before an unattended analysis. -

-
-
-
-

IDA

-
-

- The verified IDA setups use Windows, with an existing MCP - registration for an attached GUI or a headless database. - Linux and macOS headless analysis remains unverified. -

-
- Connect an existing IDA MCP registration -

- Keep the upstream server's working command, - args and optional env in a JSON - file. An existing mcpServers object with an - ida-pro-mcp entry is also accepted. Point REA - at that file before setup: -

-
-
- IDA connection · Windows PowerShell
-
$env:REA_IDA_MCP_CONFIG = "C:/analysis/ida-mcp.json"
+                      

+ Replace both paths with your existing installations. + Setup checks them and records valid settings for your + selected client. On macOS, use a Ghidra package with the + native decompiler matching your host architecture. +

+
+
+
+
+

Hopper

+
+

+ Connect Hopper on macOS or Linux. Run the setup command + above; it detects an existing installation or offers an + installation plan for your approval. +

+

+ On macOS, complete Hopper's first-run choice of demo mode + or license activation before an unattended analysis. +

+
+
+
+

IDA

+
+

+ The verified IDA setups use Windows, with an existing MCP + registration for an attached GUI or a headless database. + Linux and macOS headless analysis remains unverified. +

+
+ + Connect an existing IDA MCP registration + +

+ Keep the upstream server's working command, + args and optional env in a + JSON file. An existing mcpServers object + with an ida-pro-mcp entry is also accepted. + Point REA at that file before setup: +

+
+
+ IDA connection · Windows PowerShell +
+
$env:REA_IDA_MCP_CONFIG = "C:/analysis/ida-mcp.json"
 $env:REA_ANALYSIS_PROVIDER = "ida"
 npx -y rea-agents@latest doctor --provider ida --json
 npx rea-agents@latest setup
-
-

- The attached profile uses upstream legacy 1.4.0 tools with - the original binary already open in IDA. The headless - profile uses the upstream database-supervisor API and a - registration with "mode": "headless"; real - verification covers an IDA 9.3 Windows x64 supervisor. - Doctor checks the registration without starting IDA. -

-

- Give REA the original executable path, with - --provider ida in a CLI query. For an agent, - include REA_IDA_MCP_CONFIG in the REA - registration's environment and select IDA when opening the - binary. -

-
+
+

+ The attached profile uses upstream legacy 1.4.0 tools + with the original binary already open in IDA. The + headless profile uses the upstream database-supervisor + API and a registration with + "mode": "headless"; real verification + covers an IDA 9.3 Windows x64 supervisor. Doctor checks + the registration without starting IDA. +

+

+ Give REA the original executable path, with + --provider ida in a CLI query. For an + agent, include REA_IDA_MCP_CONFIG in the + REA registration's environment and select IDA when + opening the binary. +

+
+
-
+

For an existing Ghidra setup, inspect a function with:

@@ -308,15 +307,21 @@ npx rea-agents@latest setup
Copy -
rea function /absolute/path/to/program main \
+              
npx -y rea-agents@latest function /absolute/path/to/program main \
   --provider ghidra --json

Replace main with the function name or address you - want to inspect. Each CLI invocation imports and analyzes the - target in its own session. Your agent's MCP queries reuse the - imported binary while that session is open. + want to inspect.

+
+ How native analysis sessions work +

+ Each CLI command imports and analyzes the binary, then closes + its session. Your agent's MCP queries reuse the imported binary + while that session is open. +

+

Follow a native analysis example to see how the function result leads to a recovered calculation. @@ -327,8 +332,8 @@ npx rea-agents@latest setup

Getting help

If your agent doesn't see REA

- Restart or reconnect the client after setup. To check its - registration, use a client-scoped report; for Codex: + Restart or reconnect the client after setup. Check its REA + connection with doctor; for Codex:

@@ -362,6 +367,10 @@ npx rea-agents@latest setup

The report gives the failed check and a suggested next step.

+ + Top + diff --git a/website/public/guides/browser/index.html b/website/public/guides/browser/index.html index 6c643bd0e8..095c82b3fd 100644 --- a/website/public/guides/browser/index.html +++ b/website/public/guides/browser/index.html @@ -13,7 +13,7 @@ - + @@ -49,7 +49,7 @@
  • Open the browser
  • Ask your agent
  • Capture from CLI
  • -
  • Read and follow
  • +
  • Read the request and script
  • @@ -62,12 +62,8 @@ notes.json, formats the two notes as CSV, and starts a download.

    -

    - We want to see which request the click makes, then use the script - to understand how the response becomes a file. -

    -

    One action, captured while it happens

    +

    From the click to the CSV download

    1. 01 · YouGET notes.json → 200 → notes[] → CSV

      - The request and response metadata come from the capture. CSV - construction is explained by reading the script. + Start the capture before clicking Export notes.
    -

    Open an observable browser

    +

    Open Chrome with remote debugging

    - Start a Chrome-family browser with a dedicated profile and a local - debugging port. On Linux, for this site's local preview: + Open the hosted Notes example with a separate browser profile and + a local debugging port. On Linux:

    @@ -125,12 +120,12 @@
    google-chrome \
       --remote-debugging-port=9222 \
       --user-data-dir=/tmp/rea-notes-browser \
    -  http://127.0.0.1:4173/examples/notes-web/
    + https://morluto.github.io/rea/examples/notes-web/

    Use your installed browser's executable path on macOS or Windows. - The page URL should point to your local preview or - the hosted Notes example. + You can replace the page URL with another page you want to + inspect.

    REA connects to this existing browser. You keep control of the @@ -225,7 +220,7 @@

    -

    Read the result, then follow the code

    +

    Read the request and export script

    @@ -260,6 +255,10 @@
    +

    + The hosted example's request path includes the site's + /rea/ prefix. +

    Read the loaded scripts

    + + Top + diff --git a/website/public/guides/index.html b/website/public/guides/index.html index 93780a1930..d6c953d4d2 100644 --- a/website/public/guides/index.html +++ b/website/public/guides/index.html @@ -13,7 +13,7 @@ - +
    -

    Start with
    an example.

    +

    REA guides.

    - Follow an example from the first prompt to the evidence. Then use the - same approach with your own app. + Analyze a native function, an Electron export or a browser request. + Each guide includes a prompt, code and results.

    @@ -70,7 +70,9 @@ Preload API IPC CSV file
    - Application workflows + JavaScript & Electron guide

    For a larger packaged app, follow Notion's clipboard bridge. @@ -96,12 +98,8 @@

    First investigation

    -

    Connect your agent, then ask.

    -

    - Each guide starts with a plain-language question and follows a - concrete example. If you haven't connected REA yet, setup takes - you through the client and provider choices. -

    +

    Set up REA

    +

    Connect your coding agent or use REA from the terminal.

    Get started with REA
    @@ -119,5 +117,8 @@

    + + Top + diff --git a/website/public/guides/javascript/index.html b/website/public/guides/javascript/index.html index f52bb2161a..e7186f0ce5 100644 --- a/website/public/guides/javascript/index.html +++ b/website/public/guides/javascript/index.html @@ -13,7 +13,7 @@ - + @@ -35,10 +35,10 @@ Home / Guides / JavaScript & Electron

    -

    Follow a feature
    across the app.

    +

    Trace an Electron
    CSV export.

    - Use a CSV export to see how REA connects JavaScript files, exposed - APIs and Electron process boundaries. + Follow a button click from the page API, through Electron IPC, to the + CSV file writer.

    @@ -47,9 +47,9 @@
    @@ -62,7 +62,9 @@ across several files.

    -

    A feature across two processes

    +

    + The CSV export across Electron processes +

    1. Renderer → preload
    -

    Give your agent one feature

    +

    Ask about the CSV export

    Connect your agent to REA and give it the app directory or ASAR file: @@ -140,7 +142,7 @@

    -

    Connect the pieces

    +

    Trace the export code

    The page calls a small API

    @@ -266,10 +268,10 @@ /absolute/path/to/notes-example --json

    - Replace the path with the folder you unzipped. Look for - normalized_result.summary.ipc and the source - locations in normalized_result.graph. The example has - one channel and one paired invocation. + Replace the path with the folder you unzipped. You should find one + IPC channel and one matching handler in + normalized_result.summary.ipc. Source locations are + in normalized_result.graph.

    For your own app, use the same command with its directory or ASAR @@ -278,7 +280,7 @@

    -

    Ask the next question

    +

    Inspect the CSV writer

    Follow notes:export to its handler. Which inputs @@ -328,5 +330,8 @@

    + + Top + diff --git a/website/public/guides/native/index.html b/website/public/guides/native/index.html index 48237b101c..ba28535c3c 100644 --- a/website/public/guides/native/index.html +++ b/website/public/guides/native/index.html @@ -13,7 +13,7 @@ - + @@ -34,10 +34,10 @@ -

    Recover a calculation.
    Follow the evidence.

    +

    Recover DX-Ball's
    sound-pan calculation.

    - Use DX-Ball’s sound panning to see how an agent moves from a binary to - an explanation and a checked C function. + Inspect the original instructions, identify the input and constants, + then check the recovered C.

    @@ -46,18 +46,17 @@
    -

    A position becomes a sound

    +

    Calculate left/right sound panning

    In DX-Ball, a brick hit supplies a horizontal screen position. The - sound code turns it into a left/right panning value. We want to - recover that calculation from the executable. + sound code turns it into a left/right panning value.

    The starting function, 0x00406400, has a short @@ -138,14 +137,9 @@ Example prompt. Give your agent the path to your local DX-Ball 1.07 executable.

    -

    - You can start with a feature name. Your agent uses REA to locate - relevant functions, then follows the evidence with more focused - queries. -

    -

    Follow what REA returns

    +

    Read the REA results

    Read the input

    @@ -272,5 +266,8 @@ return (DxBallInt)pan;

    + + Top + diff --git a/website/public/index.html b/website/public/index.html index d81f55447b..c08ccca61c 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -13,7 +13,7 @@ - +
    +
    +
    +
    +

    Case study · CTF

    +

    Solve a CTF flag checker

    +

    + Use REA to inspect an easy DownUnderCTF binary. Extract its + checking rules, solve for the flag and confirm that the original + program accepts it. +

    + See the CTF solution +
    +
    +

    One check reveals one character

    +
    +
    + From REA's code and data +
    +
    Mask: skip 21, select 1, skip 14
    +Target: 55
    +
    +code[21] = 55  →  '7'
    +
    +
    + Each mask selects characters whose codes must add up to a stored + number. This one selects a single character. +
    +
    +
    +
    +
    @@ -289,5 +321,8 @@

    + + Top + diff --git a/website/public/showcase/ctf/index.html b/website/public/showcase/ctf/index.html new file mode 100644 index 0000000000..e7055038e9 --- /dev/null +++ b/website/public/showcase/ctf/index.html @@ -0,0 +1,620 @@ + + + + + + Solve a CTF flag checker — REA + + + + + + + + + +
    +
    + +

    Case study · DownUnderCTF 2023

    +

    Find the flag in
    a CTF binary.

    +

    + Use REA to inspect a small flag checker, turn its rules into equations + and find an input that makes the original program print + Correct!. +

    +

    + masked squares flag checker · Easy · Linux x86-64 +

    + View the original challenge +
    + +
    +

    From a binary to a checked answer

    +
      +
    1. + 01 · Find the checkStrings → checking functionsREA finds the prompt, its references and the code that accepts or + rejects an input. +
    2. +
    3. + 02 · Extract the rules36 characters · 26 sumsREA returns the instructions and data behind each check. The + agent turns them into equations. +
    4. +
    5. + 03 · Solve and runRecovered flag → Correct!A Python script solves the equations. REA captures the original + program accepting the result. +
    6. +
    +
    + The challenge is by joseph, published in DownUnderCTF's official + repository. +
    +
    + +
    + +
    +
    +

    Find the checking code

    +

    + The handout is a 15 KB executable. It asks for a flag, then prints + Correct! or Incorrect!. Start with a + simple request to your agent. +

    +
    +
    + Your coding agent + +
    +

    + Use REA to analyze ms_flag_checker and find the flag. Explain + how the checks work, then verify the answer with the original + program. +

    +
    +

    + An example prompt, with the downloaded challenge available locally + and a native analysis provider connected. +

    +
      +
    1. +

      Follow the input prompt to its function

      +
      +
      +
      Agent → REA
      +
      open_binary
      +{"path": "ms_flag_checker"}
      +
      +search_strings
      +{"pattern": "flag|correct|wrong|input",
      + "mode": "regex", "case_sensitive": false}
      +
      +xrefs
      +{"address": "0x102004"}
      +
      +resolve_containing_procedure
      +{"address": "0x10126d"}
      +
      +
      +
      + REA → agent · selected results +
      +
      0x102004  "What is the flag? "
      +0x10201c  "Incorrect!"
      +0x102027  "Correct!"
      +
      +Prompt referenced at: 0x10126d
      +Containing function: 0x10124d
      +
      +
      +

      + The reference gives the agent a place to investigate, even + though the executable's original function names have been + removed. +

      +
    2. +
    3. +

      Read the checker and its two helpers

      +
      +
      +
      Agent → REA
      +
      analyze_function
      +{"procedure": "0x10124d"}
      +
      +analyze_function
      +{"procedure": "0x101189"}
      +
      +analyze_function
      +{"procedure": "0x101217"}
      +
      +
      +
      + REA → agent · selected instructions +
      +
      0x1012c1: CMP EDI, 0x24
      +0x1012c6: LEA RBX, [0x1040e0]
      +0x1012cd: LEA RBP, [0x104060]
      +0x1012d4: LEA R13, [RBX + 0x270]
      +0x1012e1: CALL 0x00101189
      +0x1012ec: CALL 0x00101217
      +0x1012f1: CMP dword ptr [RBP], EAX
      +0x1012f4: JNZ 0x00101335
      +0x1012f6: ADD RBX, 0x18
      +
      +
      +

      + The first loop copies 36 character codes. The checking loop + decodes a mask, sums the selected codes and compares the + result with a stored number. Its 0x18-byte steps + across 0x270 bytes give 26 masks. +

      +
    4. +
    +

    + These are selected results from a fresh REA analysis. The local + target path is shortened for display; addresses refer to REA's + imported image. +

    +
    + +
    +

    One check reveals one character

    +

    + The checker arranges the input in a 6×6 grid. A mask chooses which + cells contribute to a sum. Its compact encoding uses negative + numbers to skip cells and positive numbers to select them. +

    +
    +
    + The seventh mask skips 21 cells, selects position 21, then skips 14. Only that cell contributes to the sum. Its required character code is 55, which means the character is 7. +
    +
    + The seventh check selects just one position. + Open figure. +
    +
    +

    + Read the mask and its target from the binary +

    +
    +
    +
    + Agent → REA · read_bytes +
    +
    {"address": "0x1040e0", "length": 624}
    +{"address": "0x104060", "length": 144}
    +
    +
    +
    + REA → agent · seventh entries +
    +
    Mask at 0x104170:
    +eb 01 f2 00  →  -21, +1, -14, stop
    +
    +Target at 0x104078:
    +37 00 00 00  →  55
    +
    +
    +

    + The instruction MOVSX reads mask bytes as signed + values. This is why eb means −21. The target is a + little-endian integer. Together, these bytes tell the agent that + code[21] = 55, so that character is 7. +

    +
    + +
    +

    From instructions to a readable check

    +

    + Select a step to connect the summing helper and its caller to the + same rule in C. +

    +
    +
    + + + + + +
    +
    +
    +
    + REA · selected original instructions +
    +
    0x10121c: MOV ECX, 0x0
    +
    +0x10122c: CMP dword ptr [RSI + RAX*0x1], 0x0
    +0x101230: JZ 0x00101223
    +
    +0x101232: ADD ECX, dword ptr [RDI + RAX*0x1]
    +
    +0x101223: ADD RAX, 0x4
    +
    +0x1012f1: CMP dword ptr [RBP], EAX
    +0x1012f4: JNZ 0x00101335
    +
    +
    +
    + Readable C · one-check summary +
    +
    int check_one_mask(const int codes[36],
    +                   const int mask[36],
    +                   int target) {
    +    int total = 0;
    +    for (int i = 0; i < 36; ++i) {
    +        if (mask[i]) {
    +            total += codes[i];
    +        }
    +    }
    +    return total == target;
    +}
    +
    +
    +
    +

    + 01 · Start at zero. ECX holds + the running sum. +

    + + + + +
    +
    +

    + The instructions come from the summing helper and its caller. The + C is an explanatory summary with descriptive names and a flattened + 36-cell loop. +

    +
    + +
    +

    Solve the 26 equations

    +

    + Most masks select several cells. For example, the first check adds + 16 character codes and requires a total of 1441. The overlapping + selections give equations that a small Python script can solve + together. +

    +
    +
    + Python · solver excerpt +
    +
    from z3 import Int, Or, Solver, Sum, sat
    +
    +codes = [Int(f"code_{i}") for i in range(36)]
    +solver = Solver()
    +alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
    +for code in codes:
    +    solver.add(Or(*[code == ord(c) for c in alphabet]))
    +for code, char in zip(codes, "DUCTF{"):
    +    solver.add(code == ord(char))
    +solver.add(codes[-1] == ord("}"))
    +
    +for selected, target in checks:
    +    solver.add(Sum([codes[i] for i in selected]) == target)
    +
    +assert solver.check() == sat
    +model = solver.model()
    +print("".join(chr(model.eval(code).as_long()) for code in codes))
    +
    +

    + Z3 is a solver that finds values satisfying these equations. This + excerpt uses the extracted checks and assumes a + DUCTF{…} flag with letters, digits, underscores and + braces. The complete download includes all 26 masks and targets. +

    + Download the complete solver +
    + +
    +

    Check the answer with the original program

    +

    + REA's process capture records the original executable accepting + the solved flag. Changing one selected character from + z to y makes the first sum 1440 instead + of 1441, and the program rejects it. +

    +
    + + + + + + + + + + + + + + + + + + + + +
    InputObserved outputExit code
    Solver's flagCorrect!0
    One character changedIncorrect!255
    +
    +
    + Show the recovered flag and captured output +
    +
    + REA · original program output +
    +
    What is the flag? DUCTF{ezzzpzzz_07bcda7bfe81faf43caa}
    +Correct!
    +
    +

    + The recovered value also matches the flag in the organizers' + published challenge metadata, checked after solving the binary. +

    +
    +
    + Source, target identity and analysis details +

    + Fresh analysis and two process captures on 8 October 2026, using + REA 4.1.0 with Ghidra 12.1.4 on Linux x64. The binary is a + stripped x86-64 ELF, 15,248 bytes. +

    +
    +
    + Original executable · SHA-256 +
    +
    dcd3bec4f608e11f3a12ce461100aaf5f621851ee63a1f870d7c90d4e3dd51ae
    +
    +

    + The string references, functions and constants were inspected + before reading the organizers' source or solution. The Python + solver and the diagram were built from those REA results. +

    +

    + Compare with the organizers' source + · + Challenge metadata and flag + · + REA evidence notes +

    +
    +
    + +
    +

    Try it yourself

    +

    + Download the original challenge and the solver into one folder. + Use the prompt above to investigate with your agent, or run the + included script. +

    + +
    +
    + Terminal · solve and run +
    +
    python3 -m venv .venv
    +.venv/bin/python -m pip install z3-solver
    +.venv/bin/python solve.py
    +
    +chmod +x ms_flag_checker
    +./ms_flag_checker
    +
    +

    + The script prints a candidate flag; paste it at the program's + prompt. Solving needs Python and z3-solver. Running + the original executable needs Linux x86-64. +

    + Set up REA with your coding agent +
    +
    +
    +
    + +

    + Top + + diff --git a/website/public/showcase/ctf/solve.py b/website/public/showcase/ctf/solve.py new file mode 100644 index 0000000000..733e045ef0 --- /dev/null +++ b/website/public/showcase/ctf/solve.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Solve the DownUnderCTF 2023 masked-squares handout from REA-extracted checks. + +Requires Python 3 and z3-solver. The constants below were read from the original +ELF with SHA-256 dcd3bec4f608e11f3a12ce461100aaf5f621851ee63a1f870d7c90d4e3dd51ae. +Challenge by joseph / DownUnderCTF: +https://github.com/DownUnderCTF/Challenges_2023_Public/tree/b48f83db2984bc7fa2ec7879a32d7efb1be8d333/rev/masked-squares-flag-checker +""" + +from z3 import Int, Or, Solver, Sum, sat + +TARGETS = [1441, 2043, 1259, 2031, 1799, 746, 55, 1450, 1485, 1362, 611, 1314, 358, 1834, 1500, 1355, 2011, 1990, 1939, 1990, 278, 1859, 2111, 1510, 888, 1224] +RUNS = [ + (-5, 1, -2, 1, -1, 3, -3, 2, -1, 1, -1, 1, -1, 1, -1, 2, -3, 2, -2, 2), + (-1, 1, -3, 2, -3, 2, -1, 1, -2, 3, -1, 1, -1, 7, -1, 3, -1, 2), + (-1, 1, -1, 1, -1, 2, -1, 1, -6, 1, -1, 1, -2, 1, -1, 1, -1, 1, -4, 3, -1, 1, -2), + (2, -1, 5, -3, 3, -1, 4, -1, 2, -1, 5, -3, 2, -3), + (-1, 1, -3, 1, -1, 1, -2, 1, -1, 3, -1, 1, -1, 1, -2, 5, -1, 4, -1, 1, -3), + (1, -2, 2, -8, 1, -7, 2, -1, 1, -3, 1, -2, 1, -4), + (-21, 1, -14), + (-2, 3, -4, 4, -5, 1, -2, 1, -1, 1, -2, 1, -2, 1, -1, 4, -1), + (-4, 5, -3, 1, -5, 1, -3, 1, -2, 5, -4, 2), + (-1, 2, -4, 1, -1, 4, -3, 1, -4, 1, -1, 2, -1, 1, -3, 1, -1, 2, -2), + (-2, 1, -8, 1, -1, 1, -3, 1, -1, 1, -7, 1, -8), + (1, -2, 5, -1, 1, -8, 3, -2, 1, -2, 1, -3, 1, -4, 1), + (-3, 1, -9, 1, -2, 1, -17, 1, -1), + (3, -5, 4, -1, 1, -2, 1, -1, 2, -3, 3, -1, 1, -1, 3, -1, 2, -1), + (-2, 2, -3, 1, -1, 1, -1, 1, -1, 1, -7, 1, -1, 6, -1, 3, -1, 1, -1), + (1, -5, 1, -2, 2, -1, 2, -1, 1, -1, 1, -5, 1, -1, 1, -1, 5, -4), + (2, -3, 1, -5, 4, -2, 3, -2, 5, -1, 2, -1, 2, -1, 2), + (-1, 1, -3, 1, -1, 2, -2, 1, -1, 7, -2, 3, -1, 4, -1, 1, -2, 1, -1), + (2, -1, 1, -2, 2, -1, 2, -2, 5, -1, 1, -1, 2, -3, 2, -1, 1, -2, 2, -1, 1), + (1, -3, 3, -1, 1, -2, 1, -1, 6, -1, 1, -1, 2, -2, 1, -1, 1, -1, 3, -1, 2), + (-9, 1, -9, 1, -5, 1, -10), + (1, -1, 2, -2, 1, -1, 9, -1, 1, -1, 1, -7, 2, -2, 1, -2, 1), + (-1, 9, -1, 3, -1, 6, -1, 1, -1, 1, -2, 1, -3, 1, -4), + (-2, 1, -3, 1, -1, 2, -5, 6, -1, 2, -2, 4, -1, 1, -4), + (-2, 1, -4, 1, -1, 1, -2, 1, -3, 1, -3, 1, -3, 3, -6, 1, -2), + (-8, 4, -2, 2, -1, 3, -5, 1, -2, 2, -1, 1, -4), +] + + +def decode_mask(runs): + """Expand signed run lengths into 36 selected/unselected cells.""" + cells = [selected for run in runs for selected in [run > 0] * abs(run)] + if len(cells) != 36: + raise ValueError('A mask must cover all 36 input positions.') + return [i for i, selected in enumerate(cells) if selected] + + +codes = [Int(f'code_{i}') for i in range(36)] +solver = Solver() +solver.set(timeout=60000) +# Analyst assumptions about a conventional flag's format and alphabet. +alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}' +for code in codes: + solver.add(Or(*[code == ord(char) for char in alphabet])) +for code, char in zip(codes, 'DUCTF{'): + solver.add(code == ord(char)) +solver.add(codes[-1] == ord('}')) +for runs, target in zip(RUNS, TARGETS): + solver.add(Sum([codes[i] for i in decode_mask(runs)]) == target) + +status = solver.check() +if status != sat: + raise SystemExit(f'No candidate returned: {status}. {solver.reason_unknown()}') +model = solver.model() +values = [model.eval(code).as_long() for code in codes] +if not all(sum(values[i] for i in decode_mask(runs)) == target + for runs, target in zip(RUNS, TARGETS)): + raise SystemExit('Candidate did not pass the extracted checks.') +print(''.join(map(chr, values))) diff --git a/website/public/showcase/dx-ball/index.html b/website/public/showcase/dx-ball/index.html index 7257c30c5b..12cc6854fa 100644 --- a/website/public/showcase/dx-ball/index.html +++ b/website/public/showcase/dx-ball/index.html @@ -13,7 +13,7 @@ - +
    45,380
    -
    original-x86 differential cases
    +
    comparisons with original code
    33
    -
    byte-exact functions
    +
    functions with matching compiled bytes

    @@ -112,12 +113,6 @@ game continues with ball and paddle physics, power-up handling and Windows integration.

    -

    - The analysis starts from the DX-Ball 1.07 executable. REA returns - function code, instructions, callers and state references. These - findings guide the C implementation; execution of the original x86 - functions and compiler replay check the result. -

    @@ -267,12 +262,6 @@

    -

    - REA exposes these operations through CLI and MCP, with target - identity and Evidence IDs in the results. The agent can follow up - on a caller or data address directly, then use the findings to - write maintained C. -

    From REA's findings to C

    @@ -523,10 +512,8 @@

    Checking the recovered function

    - The input, caller context and constant values returned through REA - guided the maintained C implementation. The project then checked - it against the original x86 function and the pinned compiler. For - this function, both checks pass. + The recovered function passes two checks: its return values match + the original, and its compiled bytes match the executable.

    @@ -634,5 +621,8 @@

    + + Top + diff --git a/website/public/showcase/index.html b/website/public/showcase/index.html index 88bae4637c..23a112d9b7 100644 --- a/website/public/showcase/index.html +++ b/website/public/showcase/index.html @@ -6,14 +6,14 @@ Case studies — REA - +

    + + Top + diff --git a/website/public/showcase/notion/index.html b/website/public/showcase/notion/index.html index bc4d265a6e..d5dacc1782 100644 --- a/website/public/showcase/notion/index.html +++ b/website/public/showcase/notion/index.html @@ -13,7 +13,7 @@ - +
    @@ -93,8 +93,7 @@
    @@ -294,14 +293,12 @@ function invokeInMainAndReturnResult(channel, ...args) {

    How does a copied block keep its structure?

    - The cached web editor adds a small detail to programmatic copies. - It sends text and HTML through - electronApi.clipboard.write, while keeping Notion's - private block data in a local virtual clipboard. An ID in the HTML - connects the two. + Notion's web editor sends text and HTML through + electronApi.clipboard.write. It keeps the block data + locally and puts an ID in the copied HTML to connect them.

    -

    One copy, two representations

    +

    HTML links to saved block data

    @@ -313,7 +310,7 @@ html: "<p>Project notes</p>"
    - Kept in the local virtual clipboard + Saved locally by Notion
    id: <copy-id>
     mimeTypes:
    @@ -327,76 +324,59 @@ mimeTypes:
                   >
                     
  • Read the HTML ID
  • Find the same local ID
  • -
  • Restore private formats
  • +
  • Restore Notion block data
  • - Illustrative text and ID. The saved module probe recovered the - private formats when IDs matched; a stale entry left only the - ordinary text and HTML. + Illustrative text and ID. The recorded module test restores the + block data when IDs match; a stale entry returns only text and + HTML.

    - On paste, the reader looks for the marker and checks it against - the saved copy. A matching ID reconnects the text to its richer - representation. This explains why the HTML carries more than - visible content. + On paste, Notion looks for the HTML ID and checks it against the + saved copy. Matching IDs restore the block data.

    - Web assets cached on July 13, 2026. The desktop-package bridge - above and this web-side format are separate pieces of the - investigation. -

    -
    - -
    -

    From Markdown to structured blocks

    -

    - The same investigation also recovered Notion's Markdown - processing. A small table makes the internal format easy to see: - its rows and cells become data that the block renderer can use. -

    -
    -

    - A table through Notion's Markdown parser -

    -
    -
    -
    Markdown input
    -
    | A | B |
    -| - | - |
    -| 1 | 2 |
    -
    -
    -
    - Structured parser output · excerpt -
    -
    table · headerRow: true
    -  table_row
    -    cells: ["A"], ["B"]
    -  table_row
    -    cells: ["1"], ["2"]
    -
    -
    -
    - A saved extracted-module probe of web parser - 422457. Both page and chat filters preserve this - table. The tree shows the recorded rows and cell text. -
    -
    -

    - Finding that transformation required separating the ordinary - paste/import path from the structured Markdown and AI/chat - parsers. Each has its own entry point and intermediate format. + Clipboard format recorded from web assets cached on July 13, 2026.

    -

    Try another question

    -

    - Start with one feature and follow the code that crosses a process - boundary. A named API, a literal channel and its receiver are - useful places to begin. -

    +

    More examples and sources

    +
    + Another example: Markdown tables +

    + Notion parses Markdown into block data. This example shows how a + table becomes header and data rows. +

    +
    +

    + A table through Notion's Markdown parser +

    +
    +
    +
    Markdown input
    +
    | A | B |
    +| - | - |
    +| 1 | 2 |
    +
    +
    +
    + Structured parser output · excerpt +
    +
    table · headerRow: true
    +  table_row
    +    cells: ["A"], ["B"]
    +  table_row
    +    cells: ["1"], ["2"]
    +
    +
    +
    + Recorded output from an extracted Markdown parser. The table + becomes rows and cells. +
    +
    +
    Another path: the offline SQLite connection

    @@ -436,6 +416,12 @@ mimeTypes: matching receiver are connected by reading their wrapper implementations in the package.

    +

    + The Markdown example uses parser module 422457. + Saved module tests show that both page and chat filters preserve + this table. The paste/import, structured Markdown and AI/chat + parsers have separate entry points and intermediate formats. +

    • @@ -454,7 +440,7 @@ mimeTypes:
    Start with your own targetAnalyze your own app
    @@ -473,5 +459,8 @@ mimeTypes:

    + + Top + diff --git a/website/public/showcase/th04/index.html b/website/public/showcase/th04/index.html index 07de3370ae..1fd59e7079 100644 --- a/website/public/showcase/th04/index.html +++ b/website/public/showcase/th04/index.html @@ -13,7 +13,7 @@ - +